Skip to content

Commit 1f3b8c2

Browse files
committed
chore: Rewrite test utils doc generator
1 parent 3bdf332 commit 1f3b8c2

File tree

13 files changed

+243
-57
lines changed

13 files changed

+243
-57
lines changed

fixtures/test-utils/glob-test/a.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

fixtures/test-utils/glob-test/b.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

fixtures/test-utils/glob-test/tsconfig.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"components",
1313
"schema",
1414
"test-utils",
15+
"test-utils-new",
1516
"*.js",
1617
"*.d.ts"
1718
],

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
export * from './components/interfaces';
44
export { writeComponentsDocumentation } from './components';
55
export { documentTestUtils } from './test-utils';
6+
export { writeTestUtilsDocumentation } from './test-utils-new';

src/test-utils-new/extractor.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import ts from 'typescript';
4+
import { extractDeclaration, getDescription, isOptional, stringifyType } from '../components/type-utils';
5+
import { TestUtilsDoc } from '../test-utils/interfaces';
6+
7+
function getInheritedFrom(declaration: ts.Declaration, currentClassName: string) {
8+
if (!ts.isMethodDeclaration(declaration) || !ts.isClassDeclaration(declaration.parent) || !declaration.parent.name) {
9+
throw new Error(`Unexpected declaration parent: ${declaration.getText()}`);
10+
}
11+
const parentName = declaration.parent.name.getText();
12+
if (parentName === currentClassName) {
13+
return undefined;
14+
}
15+
return { name: parentName + '.' + declaration.name.getText() };
16+
}
17+
18+
function getDefaultValue(declaration: ts.Declaration) {
19+
if (!ts.isParameter(declaration)) {
20+
throw new Error(`Unexpected declaration: ${declaration.getText()}`);
21+
}
22+
if (!declaration.initializer) {
23+
return undefined;
24+
}
25+
return declaration.initializer.getText();
26+
}
27+
28+
export default function extractDocumentation(sourceFile: ts.SourceFile, checker: ts.TypeChecker): Array<TestUtilsDoc> {
29+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
30+
if (!moduleSymbol) {
31+
throw new Error(`Unable to resolve module: ${sourceFile.fileName}`);
32+
}
33+
34+
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
35+
const definitions: Array<TestUtilsDoc> = [];
36+
37+
for (const symbol of exportSymbols) {
38+
if (!(symbol.flags & ts.SymbolFlags.Class)) {
39+
throw new Error(`Exported symbol is not a class, got ${checker.symbolToString(symbol)}`);
40+
}
41+
const className = symbol.getName();
42+
const classType = checker.getTypeAtLocation(extractDeclaration(symbol));
43+
const classDefinition: TestUtilsDoc = { name: className, methods: [] };
44+
for (const property of classType.getProperties()) {
45+
const declaration = extractDeclaration(property);
46+
const modifiers = (ts.canHaveModifiers(declaration) && ts.getModifiers(declaration)) || [];
47+
if (
48+
modifiers.find(
49+
modifier => modifier.kind & ts.SyntaxKind.ProtectedKeyword || modifier.kind & ts.SyntaxKind.PrivateKeyword
50+
)
51+
) {
52+
continue;
53+
}
54+
const type = checker.getTypeAtLocation(declaration);
55+
if (type.getCallSignatures().length !== 1) {
56+
throw new Error(`Unexpected member on ${className}${property.getName()}: ${stringifyType(type, checker)}`);
57+
}
58+
const returnType = type.getCallSignatures()[0].getReturnType();
59+
classDefinition.methods.push({
60+
name: property.getName(),
61+
description: getDescription(property.getDocumentationComment(checker), declaration).text,
62+
inheritedFrom: getInheritedFrom(declaration, className),
63+
parameters: type.getCallSignatures()[0].parameters.map(parameter => {
64+
const paramType = checker.getTypeAtLocation(extractDeclaration(parameter));
65+
return {
66+
name: parameter.name,
67+
typeName: stringifyType(paramType, checker),
68+
description: getDescription(parameter.getDocumentationComment(checker), declaration).text,
69+
flags: { isOptional: isOptional(paramType) },
70+
defaultValue: getDefaultValue(extractDeclaration(parameter)),
71+
};
72+
}),
73+
returnType: { name: stringifyType(returnType, checker) },
74+
});
75+
}
76+
classDefinition.methods.sort((a, b) => a.name.localeCompare(b.name));
77+
78+
definitions.push(classDefinition);
79+
}
80+
81+
return definitions;
82+
}

src/test-utils-new/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import fs from 'node:fs';
4+
import pathe from 'pathe';
5+
import { bootstrapTypescriptProject } from '../bootstrap/typescript';
6+
import extractDocumentation from './extractor';
7+
import { TestUtilsDoc } from '../test-utils/interfaces';
8+
9+
export interface TestUtilsDocumenterOptions {
10+
tsconfigPath: string;
11+
domUtilsRoot: string;
12+
selectorsUtilsRoot: string;
13+
}
14+
15+
interface TestUtilsDefinitions {
16+
domDefinitions: Array<TestUtilsDoc>;
17+
selectorsDefinitions: Array<TestUtilsDoc>;
18+
}
19+
20+
export function documentTestUtilsNew(options: TestUtilsDocumenterOptions): TestUtilsDefinitions {
21+
const domUtilsRoot = pathe.resolve(options.domUtilsRoot);
22+
const selectorsUtilsRoot = pathe.resolve(options.selectorsUtilsRoot);
23+
const program = bootstrapTypescriptProject(options.tsconfigPath);
24+
const checker = program.getTypeChecker();
25+
26+
const domUtilsFile = program.getSourceFiles().find(file => file.fileName === domUtilsRoot);
27+
if (!domUtilsFile) {
28+
throw new Error(`File '${domUtilsRoot}' not found`);
29+
}
30+
31+
const selectorsUtilsFile = program.getSourceFiles().find(file => file.fileName === selectorsUtilsRoot);
32+
if (!selectorsUtilsFile) {
33+
throw new Error(`File '${selectorsUtilsFile}' not found`);
34+
}
35+
return {
36+
domDefinitions: extractDocumentation(domUtilsFile, checker),
37+
selectorsDefinitions: extractDocumentation(selectorsUtilsFile, checker),
38+
};
39+
}
40+
41+
export function writeTestUtilsDocumentation({
42+
outDir,
43+
...rest
44+
}: TestUtilsDocumenterOptions & { outDir: string }): void {
45+
const { domDefinitions, selectorsDefinitions } = documentTestUtilsNew(rest);
46+
fs.mkdirSync(outDir, { recursive: true });
47+
fs.writeFileSync(
48+
pathe.join(outDir, 'dom.js'),
49+
`module.exports = { classes: ${JSON.stringify(domDefinitions, null, 2)} };`
50+
);
51+
fs.writeFileSync(
52+
pathe.join(outDir, 'selectors.js'),
53+
`module.exports = { classes: ${JSON.stringify(selectorsDefinitions, null, 2)} };`
54+
);
55+
fs.copyFileSync(require.resolve('./interfaces.d.ts'), pathe.join(outDir, 'interfaces.d.ts'));
56+
const dtsTemplate = `import { TestUtilsDefinition } from './interfaces';
57+
declare const definitions: TestUtilsDefinition;
58+
export = definitions;
59+
`;
60+
fs.writeFileSync(pathe.join(outDir, 'dom.d.ts'), dtsTemplate);
61+
fs.writeFileSync(pathe.join(outDir, 'selectors.d.ts'), dtsTemplate);
62+
}

src/test-utils-new/interfaces.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
export interface Parameter {
4+
name: string;
5+
typeName?: string;
6+
description?: string;
7+
flags: { isOptional?: boolean };
8+
defaultValue?: string;
9+
}
10+
11+
export interface TestUtilMethod {
12+
name: string;
13+
description?: string;
14+
returnType?: {
15+
name: string;
16+
};
17+
parameters: Array<Parameter>;
18+
inheritedFrom?: {
19+
name: string;
20+
};
21+
}
22+
23+
export interface TestUtilsDoc {
24+
name: string;
25+
methods: Array<TestUtilMethod>;
26+
}
27+
28+
export interface TestUtilsDefinition {
29+
classes: Array<TestUtilsDoc>;
30+
}

test/test-utils/__snapshots__/doc-generation.test.ts.snap

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ exports[`Generate documentation > For simple cases 1`] = `
44
[
55
{
66
"defaultValue": undefined,
7-
"description": "
8-
9-
",
7+
"description": undefined,
108
"flags": {
119
"isOptional": false,
1210
},
@@ -25,7 +23,7 @@ exports[`Generate documentation > deal with more complex types 1`] = `
2523
"isOptional": false,
2624
},
2725
"name": "all",
28-
"typeName": "Array",
26+
"typeName": "Array<HTMLElement>",
2927
},
3028
]
3129
`;

test/test-utils/doc-generation.test.ts

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,32 @@ describe('Generate documentation', () => {
1919

2020
const noOpMethod = methods.find(method => method.name === 'noOp');
2121
expect(noOpMethod).toBeDefined();
22-
expect(noOpMethod?.returnType).toEqual({ name: 'void', type: 'intrinsic' });
22+
expect(noOpMethod?.returnType).toEqual({ name: 'void' });
2323
expect(noOpMethod?.parameters).toEqual([]);
2424
expect(noOpMethod?.description).toBeUndefined();
2525
expect(noOpMethod?.inheritedFrom).toBeUndefined();
2626

2727
const findStringMethod = methods.find(method => method.name === 'findString');
2828
expect(findStringMethod).toBeDefined();
29-
expect(findStringMethod?.returnType).toEqual({ name: 'string', type: 'intrinsic' });
29+
expect(findStringMethod?.returnType).toEqual({ name: 'string' });
3030
expect(findStringMethod?.parameters).toEqual([]);
3131
expect(findStringMethod?.description).toBe(
32-
'Finds a string.\nThe function may look trivial but people have been losing their words\nsince centuries.\n'
32+
'Finds a string.\n\nThe function may look trivial but people have been losing their words\nsince centuries.'
3333
);
3434
expect(findStringMethod?.inheritedFrom).toBeUndefined();
3535

3636
const setStringMethod = methods.find(method => method.name === 'setString');
3737
expect(setStringMethod).toBeDefined();
38-
expect(setStringMethod?.returnType).toEqual({ name: 'void', type: 'intrinsic' });
38+
expect(setStringMethod?.returnType).toEqual({ name: 'void' });
3939
expect(setStringMethod?.parameters).toMatchSnapshot();
4040
expect(setStringMethod?.description).toBe('Short Text');
4141
expect(setStringMethod?.inheritedFrom).toBeUndefined();
4242

4343
const findObjectMethod = methods.find(method => method.name === 'findObject');
4444
expect(findObjectMethod).toBeDefined();
45-
expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType', type: 'reference' });
45+
expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType' });
4646
expect(findObjectMethod?.parameters).toEqual([]);
47-
expect(findObjectMethod?.description).toBe('Short Text.\nLong Text.\n');
47+
expect(findObjectMethod?.description).toBe('Short Text.\n\nLong Text.');
4848
expect(findObjectMethod?.inheritedFrom).toBeUndefined();
4949
});
5050

@@ -61,26 +61,14 @@ describe('Generate documentation', () => {
6161

6262
const findAllMethod = methods.find(method => method.name === 'findAll');
6363
expect(findAllMethod).toBeDefined();
64-
expect(findAllMethod?.returnType).toEqual({
65-
name: 'Array',
66-
type: 'reference',
67-
typeArguments: [
68-
{
69-
type: 'reference',
70-
name: 'HTMLElement',
71-
},
72-
],
73-
});
64+
expect(findAllMethod?.returnType).toEqual({ name: 'Array<HTMLElement>' });
7465
expect(findAllMethod?.parameters).toEqual([]);
7566
expect(findAllMethod?.description).toBeUndefined();
7667
expect(findAllMethod?.inheritedFrom).toBeUndefined();
7768

7869
const setAllMethod = methods.find(method => method.name === 'setAll');
7970
expect(setAllMethod).toBeDefined();
80-
expect(setAllMethod?.returnType).toEqual({
81-
name: 'void',
82-
type: 'intrinsic',
83-
});
71+
expect(setAllMethod?.returnType).toEqual({ name: 'void' });
8472
expect(setAllMethod?.parameters).toMatchSnapshot();
8573
expect(setAllMethod?.description).toBeUndefined();
8674
expect(setAllMethod?.inheritedFrom).toBeUndefined();
@@ -89,7 +77,7 @@ describe('Generate documentation', () => {
8977
test('and deal with inheritance', () => {
9078
const results = buildTestUtilsProject('inheritance');
9179

92-
expect(results.length).toBe(2);
80+
expect(results.length).toBe(1);
9381
const classDoc = results.find(classDoc => classDoc.name === 'TestUtilWrapper');
9482

9583
expect(classDoc).toBeDefined();
@@ -121,11 +109,12 @@ describe('Generate documentation', () => {
121109
parameters: [
122110
{
123111
name: 'order',
112+
typeName: '"first" | "last"',
124113
flags: { isOptional: false },
125-
defaultValue: '"first"',
114+
defaultValue: "'first'",
126115
},
127116
],
128-
returnType: { name: 'void', type: 'intrinsic' },
117+
returnType: { name: 'void' },
129118
},
130119
{
131120
name: 'openDropdown',
@@ -137,7 +126,7 @@ describe('Generate documentation', () => {
137126
defaultValue: 'false',
138127
},
139128
],
140-
returnType: { name: 'void', type: 'intrinsic' },
129+
returnType: { name: 'void' },
141130
},
142131
{
143132
name: 'selectOption',
@@ -149,7 +138,7 @@ describe('Generate documentation', () => {
149138
defaultValue: '1',
150139
},
151140
],
152-
returnType: { name: 'void', type: 'intrinsic' },
141+
returnType: { name: 'void' },
153142
},
154143
]);
155144
});

0 commit comments

Comments
 (0)