1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { isSemVer, isURL } from 'validator';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { processComponents } from '../../../scripts/apidocs/generate';
import { extractSummaryDefault } from '../../../scripts/apidocs/output/page';
import { getProject } from '../../../scripts/apidocs/project';
// This test suite ensures, that every method
// - has working examples
// - running these do not log anything, unless the method is deprecated
// - has a valid @since tag
// - has valid @see tags
// - has proper links in the description
const tempDir = resolve(import.meta.dirname, 'temp');
const relativeImportPath = `${'../'.repeat(5)}src`;
afterAll(() => {
// Remove temp folder
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true });
}
});
const modules = processComponents(getProject());
function resolveDirToModule(moduleName: string): string {
return resolve(tempDir, moduleName);
}
function resolvePathToMethodFile(
moduleName: string,
methodName: string,
signature: number
): string {
const dir = resolveDirToModule(moduleName);
return resolve(dir, `${methodName}_${signature}.ts`);
}
const allowedReferences = new Set(
modules.flatMap(({ camelTitle, methods, category }) => {
return methods.map(({ name }) =>
category ? `faker.${camelTitle}.${name}` : `${camelTitle}.${name}`
);
})
);
const allowedLinks = new Set(
modules.flatMap(({ camelTitle, methods }) => {
return [
`/api/${camelTitle}.html`,
...methods.map(
({ name }) => `/api/${camelTitle}.html#${name.toLowerCase()}`
),
];
})
);
function assertDescription(description: string): void {
const linkRegexp = /\[([^\]]+)\]\(([^)]+)\)/g;
const links = [...description.matchAll(linkRegexp)].map((m) => m[2]);
for (const link of links) {
expect(link).toMatch(/^https?:\/\//);
expect(link).toSatisfy(isURL);
if (link.includes('fakerjs.dev/api/')) {
expect(allowedLinks, `${link} to point to a valid target`).toContain(
link.replace(/.*fakerjs.dev\//, '/')
);
}
}
}
describe('check docs completeness', () => {
it('all modules and methods are present', () => {
// This could be converted to an Object, but that would erase the order of the pages
const pageContents = modules.map((m) => [
m.camelTitle,
m.methods.map((m) => m.name),
]);
expect(pageContents).toMatchSnapshot();
});
});
describe('verify JSDoc tags', () => {
describe.each(modules.map((m) => [m.camelTitle, m]))(
'%s',
(moduleName, module) => {
describe('verify module', () => {
it('verify description', () => {
assertDescription(module.description);
});
});
describe.each(module.methods.map((m) => [m.name, m]))(
'%s',
(methodName, method) => {
describe.each(method.signatures.map((s, i) => [i, s]))(
'%i',
(signatureIndex, signature) => {
beforeAll(() => {
// Write temp files to disk
// By extracting the examples
// Guessing required imports
// And saving them to disk for later execution
const dir = resolveDirToModule(moduleName);
mkdirSync(dir, { recursive: true });
const path = resolvePathToMethodFile(
moduleName,
methodName,
signatureIndex
);
let examples = signature.examples.join('\n');
if (moduleName === 'faker' && methodName === 'constructor') {
// That case should demonstrate an error and is thus not suitable for testing
examples = examples.replace(
'customFaker.music.genre()',
'// customFaker.music.genre()'
);
}
// Replace imports for users with our source path
examples = examples.replaceAll(
" from '@faker-js/faker'",
` from '${relativeImportPath}'`
);
if (moduleName === 'randomizer') {
examples = `import { generateMersenne32Randomizer } from '${relativeImportPath}/utils/mersenne';
const randomizer = generateMersenne32Randomizer();
${examples}`;
}
// If imports are present, we expect them to be complete
if (!examples.includes('import ')) {
const imports = [
// collect the imports for the various locales e.g. fakerDE_CH
...new Set(examples.match(/(?<!\.)faker[^.-]*(?=\.)/g)),
];
if (imports.length > 0) {
examples = `import { ${imports.join(
', '
)} } from '${relativeImportPath}';\n\n${examples}`;
}
}
writeFileSync(path, examples);
});
it('verify description', () => {
assertDescription(signature.description);
});
it(
'verify @example tag',
{
retry: 3,
timeout: 30000,
},
async () => {
const examples = signature.examples.join('\n');
expect(
examples,
`${moduleName}.${methodName} to have examples`
).not.toBe('');
// Grab path to example file
const path = resolvePathToMethodFile(
moduleName,
methodName,
signatureIndex
);
// Executing the examples should not throw
await expect(
import(`${path}?scope=example`),
examples
).resolves.toBeDefined();
}
);
// This only checks whether the whole method is deprecated or not
// It does not check whether the method is deprecated for a specific set of arguments
it(
'verify @deprecated tag',
{
retry: 3,
timeout: 30000,
},
async () => {
// Grab path to example file
const path = resolvePathToMethodFile(
moduleName,
methodName,
signatureIndex
);
const consoleWarnSpy = vi.spyOn(console, 'warn');
// Run the examples
await import(`${path}?scope=deprecated`);
// Verify that deprecated methods log a warning
const { deprecated } = signature;
if (deprecated == null) {
expect(consoleWarnSpy).not.toHaveBeenCalled();
} else {
expect(consoleWarnSpy).toHaveBeenCalled();
expect(deprecated).not.toBe('');
}
consoleWarnSpy.mockRestore();
}
);
describe.each(signature.parameters.map((p) => [p.name, p]))(
'%s',
(_, parameter) => {
it('verify default value', () => {
const {
name,
default: paramDefault,
description,
} = parameter;
const commentDefault = extractSummaryDefault(description);
if (paramDefault) {
if (
/^{.*}$/.test(paramDefault) ||
paramDefault.includes('\n')
) {
expect(commentDefault).toBeUndefined();
} else if (
!name.includes('.') &&
// Skip check of defaults in descriptions if it is a paraphrased function call
(commentDefault ||
(!description.includes('Defaults to') &&
!paramDefault.includes('(')))
) {
expect(
commentDefault,
`Expect '${name}'s js implementation default to be the same as the jsdoc summary default`
).toBe(paramDefault);
}
}
});
it('verify description', () => {
assertDescription(parameter.description);
});
}
);
it('verify @see tags', () => {
for (const link of signature.seeAlsos) {
if (link.startsWith('faker.')) {
// Expected @see faker.xxx.yyy()
expect(
link,
'Expect method reference to contain ()'
).toContain('(');
expect(
link,
'Expect method reference to contain ()'
).toContain(')');
expect(
link,
"Expect method reference to have a ': ' after the parenthesis"
).toContain('): ');
expect(
link,
'Expect method reference to have a description starting with a capital letter'
).toMatch(/\): [A-Z]/);
expect(
link,
'Expect method reference to start with a standard description phrase'
).toMatch(
/\): (?:For generating |For more information about |For using |For the replacement method)/
);
expect(
link,
'Expect method reference to have a description ending with a dot'
).toMatch(/\.$/);
expect(allowedReferences).toContain(
link.replace(/\(.*/, '')
);
}
}
});
it('verify @since tag', () => {
const { since } = signature;
expect(since, '@since to be present').toBeTruthy();
expect(since).not.toBe('');
expect(since, '@since to be a valid semver').toSatisfy(
isSemVer
);
});
}
);
}
);
}
);
});
|