-
Notifications
You must be signed in to change notification settings - Fork 4
chore: Rewrite test utils doc generator #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ignore: | ||
| # deprecated code | ||
| - 'src/test-utils/**' | ||
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import ts from 'typescript'; | ||
| import { extractDeclaration, getDescription, isOptional, stringifyType } from '../components/type-utils'; | ||
| import { TestUtilsDoc } from '../test-utils/interfaces'; | ||
|
|
||
| function getInheritedFrom(declaration: ts.Declaration, currentClassName: string) { | ||
| if (!ts.isMethodDeclaration(declaration) || !ts.isClassDeclaration(declaration.parent) || !declaration.parent.name) { | ||
| throw new Error(`Unexpected declaration parent: ${declaration.getText()}`); | ||
| } | ||
| const parentName = declaration.parent.name.getText(); | ||
| if (parentName === currentClassName) { | ||
| return undefined; | ||
| } | ||
| return { name: parentName + '.' + declaration.name.getText() }; | ||
| } | ||
|
|
||
| function getDefaultValue(declaration: ts.Declaration) { | ||
| if (!ts.isParameter(declaration)) { | ||
| throw new Error(`Unexpected declaration: ${declaration.getText()}`); | ||
| } | ||
| if (!declaration.initializer) { | ||
| return undefined; | ||
| } | ||
| return declaration.initializer.getText(); | ||
| } | ||
|
|
||
| export default function extractDocumentation(sourceFile: ts.SourceFile, checker: ts.TypeChecker): Array<TestUtilsDoc> { | ||
| const moduleSymbol = checker.getSymbolAtLocation(sourceFile); | ||
| if (!moduleSymbol) { | ||
| throw new Error(`Unable to resolve module: ${sourceFile.fileName}`); | ||
| } | ||
|
|
||
| const exportSymbols = checker.getExportsOfModule(moduleSymbol); | ||
| const definitions: Array<TestUtilsDoc> = []; | ||
|
|
||
| for (const symbol of exportSymbols) { | ||
| if (!(symbol.flags & ts.SymbolFlags.Class)) { | ||
| throw new Error(`Exported symbol is not a class, got ${checker.symbolToString(symbol)}`); | ||
| } | ||
| const className = symbol.getName(); | ||
| const classType = checker.getTypeAtLocation(extractDeclaration(symbol)); | ||
| const classDefinition: TestUtilsDoc = { name: className, methods: [] }; | ||
| for (const property of classType.getProperties()) { | ||
| const declaration = extractDeclaration(property); | ||
| const modifiers = (ts.canHaveModifiers(declaration) && ts.getModifiers(declaration)) || []; | ||
| if ( | ||
| modifiers.find( | ||
| modifier => modifier.kind & ts.SyntaxKind.ProtectedKeyword || modifier.kind & ts.SyntaxKind.PrivateKeyword | ||
jperals marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| ) { | ||
| continue; | ||
| } | ||
| const type = checker.getTypeAtLocation(declaration); | ||
| if (type.getCallSignatures().length !== 1) { | ||
| throw new Error(`Unexpected member on ${className} – ${property.getName()}: ${stringifyType(type, checker)}`); | ||
| } | ||
| const returnType = type.getCallSignatures()[0].getReturnType(); | ||
| classDefinition.methods.push({ | ||
| name: property.getName(), | ||
| description: getDescription(property.getDocumentationComment(checker), declaration).text, | ||
| inheritedFrom: getInheritedFrom(declaration, className), | ||
| parameters: type.getCallSignatures()[0].parameters.map(parameter => { | ||
| const paramType = checker.getTypeAtLocation(extractDeclaration(parameter)); | ||
| return { | ||
| name: parameter.name, | ||
| typeName: stringifyType(paramType, checker), | ||
| description: getDescription(parameter.getDocumentationComment(checker), declaration).text, | ||
| flags: { isOptional: isOptional(paramType) }, | ||
| defaultValue: getDefaultValue(extractDeclaration(parameter)), | ||
| }; | ||
| }), | ||
| returnType: { name: stringifyType(returnType, checker) }, | ||
| }); | ||
| } | ||
| classDefinition.methods.sort((a, b) => a.name.localeCompare(b.name)); | ||
|
|
||
| definitions.push(classDefinition); | ||
| } | ||
|
|
||
| return definitions; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import fs from 'node:fs'; | ||
| import pathe from 'pathe'; | ||
| import { bootstrapTypescriptProject } from '../bootstrap/typescript'; | ||
| import extractDocumentation from './extractor'; | ||
| import { TestUtilsDoc } from '../test-utils/interfaces'; | ||
|
|
||
| export interface TestUtilsDocumenterOptions { | ||
| tsconfigPath: string; | ||
| domUtilsRoot: string; | ||
| selectorsUtilsRoot: string; | ||
| } | ||
|
|
||
| interface TestUtilsDefinitions { | ||
| domDefinitions: Array<TestUtilsDoc>; | ||
| selectorsDefinitions: Array<TestUtilsDoc>; | ||
| } | ||
|
|
||
| export function documentTestUtilsNew(options: TestUtilsDocumenterOptions): TestUtilsDefinitions { | ||
| const domUtilsRoot = pathe.resolve(options.domUtilsRoot); | ||
| const selectorsUtilsRoot = pathe.resolve(options.selectorsUtilsRoot); | ||
| const program = bootstrapTypescriptProject(options.tsconfigPath); | ||
| const checker = program.getTypeChecker(); | ||
|
|
||
| const domUtilsFile = program.getSourceFiles().find(file => file.fileName === domUtilsRoot); | ||
| if (!domUtilsFile) { | ||
| throw new Error(`File '${domUtilsRoot}' not found`); | ||
| } | ||
|
|
||
| const selectorsUtilsFile = program.getSourceFiles().find(file => file.fileName === selectorsUtilsRoot); | ||
| if (!selectorsUtilsFile) { | ||
| throw new Error(`File '${selectorsUtilsFile}' not found`); | ||
| } | ||
| return { | ||
| domDefinitions: extractDocumentation(domUtilsFile, checker), | ||
| selectorsDefinitions: extractDocumentation(selectorsUtilsFile, checker), | ||
| }; | ||
| } | ||
|
|
||
| export function writeTestUtilsDocumentation({ | ||
| outDir, | ||
| ...rest | ||
| }: TestUtilsDocumenterOptions & { outDir: string }): void { | ||
| const { domDefinitions, selectorsDefinitions } = documentTestUtilsNew(rest); | ||
| fs.mkdirSync(outDir, { recursive: true }); | ||
| fs.writeFileSync( | ||
| pathe.join(outDir, 'dom.js'), | ||
| `module.exports = { classes: ${JSON.stringify(domDefinitions, null, 2)} };` | ||
| ); | ||
| fs.writeFileSync( | ||
| pathe.join(outDir, 'selectors.js'), | ||
| `module.exports = { classes: ${JSON.stringify(selectorsDefinitions, null, 2)} };` | ||
| ); | ||
| fs.copyFileSync(require.resolve('./interfaces.d.ts'), pathe.join(outDir, 'interfaces.d.ts')); | ||
| const dtsTemplate = `import { TestUtilsDefinition } from './interfaces'; | ||
| declare const definitions: TestUtilsDefinition; | ||
| export = definitions; | ||
| `; | ||
jperals marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fs.writeFileSync(pathe.join(outDir, 'dom.d.ts'), dtsTemplate); | ||
| fs.writeFileSync(pathe.join(outDir, 'selectors.d.ts'), dtsTemplate); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| export interface Parameter { | ||
| name: string; | ||
| typeName?: string; | ||
| description?: string; | ||
| flags: { isOptional?: boolean }; | ||
| defaultValue?: string; | ||
| } | ||
|
|
||
| export interface TestUtilMethod { | ||
| name: string; | ||
| description?: string; | ||
| returnType?: { | ||
| name: string; | ||
| }; | ||
| parameters: Array<Parameter>; | ||
| inheritedFrom?: { | ||
| name: string; | ||
| }; | ||
| } | ||
|
|
||
| export interface TestUtilsDoc { | ||
| name: string; | ||
| methods: Array<TestUtilMethod>; | ||
| } | ||
|
|
||
| export interface TestUtilsDefinition { | ||
| classes: Array<TestUtilsDoc>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,32 +19,32 @@ describe('Generate documentation', () => { | |
|
|
||
| const noOpMethod = methods.find(method => method.name === 'noOp'); | ||
| expect(noOpMethod).toBeDefined(); | ||
| expect(noOpMethod?.returnType).toEqual({ name: 'void', type: 'intrinsic' }); | ||
| expect(noOpMethod?.returnType).toEqual({ name: 'void' }); | ||
| expect(noOpMethod?.parameters).toEqual([]); | ||
| expect(noOpMethod?.description).toBeUndefined(); | ||
| expect(noOpMethod?.inheritedFrom).toBeUndefined(); | ||
|
|
||
| const findStringMethod = methods.find(method => method.name === 'findString'); | ||
| expect(findStringMethod).toBeDefined(); | ||
| expect(findStringMethod?.returnType).toEqual({ name: 'string', type: 'intrinsic' }); | ||
| expect(findStringMethod?.returnType).toEqual({ name: 'string' }); | ||
| expect(findStringMethod?.parameters).toEqual([]); | ||
| expect(findStringMethod?.description).toBe( | ||
| 'Finds a string.\nThe function may look trivial but people have been losing their words\nsince centuries.\n' | ||
| 'Finds a string.\n\nThe function may look trivial but people have been losing their words\nsince centuries.' | ||
| ); | ||
| expect(findStringMethod?.inheritedFrom).toBeUndefined(); | ||
|
|
||
| const setStringMethod = methods.find(method => method.name === 'setString'); | ||
| expect(setStringMethod).toBeDefined(); | ||
| expect(setStringMethod?.returnType).toEqual({ name: 'void', type: 'intrinsic' }); | ||
| expect(setStringMethod?.returnType).toEqual({ name: 'void' }); | ||
| expect(setStringMethod?.parameters).toMatchSnapshot(); | ||
| expect(setStringMethod?.description).toBe('Short Text'); | ||
| expect(setStringMethod?.inheritedFrom).toBeUndefined(); | ||
|
|
||
| const findObjectMethod = methods.find(method => method.name === 'findObject'); | ||
| expect(findObjectMethod).toBeDefined(); | ||
| expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType', type: 'reference' }); | ||
| expect(findObjectMethod?.returnType).toEqual({ name: 'TestReturnType' }); | ||
| expect(findObjectMethod?.parameters).toEqual([]); | ||
| expect(findObjectMethod?.description).toBe('Short Text.\nLong Text.\n'); | ||
| expect(findObjectMethod?.description).toBe('Short Text.\n\nLong Text.'); | ||
| expect(findObjectMethod?.inheritedFrom).toBeUndefined(); | ||
| }); | ||
|
|
||
|
|
@@ -61,26 +61,14 @@ describe('Generate documentation', () => { | |
|
|
||
| const findAllMethod = methods.find(method => method.name === 'findAll'); | ||
| expect(findAllMethod).toBeDefined(); | ||
| expect(findAllMethod?.returnType).toEqual({ | ||
| name: 'Array', | ||
| type: 'reference', | ||
| typeArguments: [ | ||
| { | ||
| type: 'reference', | ||
| name: 'HTMLElement', | ||
| }, | ||
| ], | ||
| }); | ||
| expect(findAllMethod?.returnType).toEqual({ name: 'Array<HTMLElement>' }); | ||
| expect(findAllMethod?.parameters).toEqual([]); | ||
| expect(findAllMethod?.description).toBeUndefined(); | ||
| expect(findAllMethod?.inheritedFrom).toBeUndefined(); | ||
|
|
||
| const setAllMethod = methods.find(method => method.name === 'setAll'); | ||
| expect(setAllMethod).toBeDefined(); | ||
| expect(setAllMethod?.returnType).toEqual({ | ||
| name: 'void', | ||
| type: 'intrinsic', | ||
| }); | ||
| expect(setAllMethod?.returnType).toEqual({ name: 'void' }); | ||
| expect(setAllMethod?.parameters).toMatchSnapshot(); | ||
| expect(setAllMethod?.description).toBeUndefined(); | ||
| expect(setAllMethod?.inheritedFrom).toBeUndefined(); | ||
|
|
@@ -89,7 +77,7 @@ describe('Generate documentation', () => { | |
| test('and deal with inheritance', () => { | ||
| const results = buildTestUtilsProject('inheritance'); | ||
|
|
||
| expect(results.length).toBe(2); | ||
| expect(results.length).toBe(1); | ||
|
Comment on lines
-92
to
+80
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Private wrappers (like this one) are not documented anymore |
||
| const classDoc = results.find(classDoc => classDoc.name === 'TestUtilWrapper'); | ||
|
|
||
| expect(classDoc).toBeDefined(); | ||
|
|
@@ -121,11 +109,12 @@ describe('Generate documentation', () => { | |
| parameters: [ | ||
| { | ||
| name: 'order', | ||
| typeName: '"first" | "last"', | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Look, the new version can detect types which the previous one could not |
||
| flags: { isOptional: false }, | ||
| defaultValue: '"first"', | ||
| defaultValue: "'first'", | ||
| }, | ||
| ], | ||
| returnType: { name: 'void', type: 'intrinsic' }, | ||
| returnType: { name: 'void' }, | ||
| }, | ||
| { | ||
| name: 'openDropdown', | ||
|
|
@@ -137,7 +126,7 @@ describe('Generate documentation', () => { | |
| defaultValue: 'false', | ||
| }, | ||
| ], | ||
| returnType: { name: 'void', type: 'intrinsic' }, | ||
| returnType: { name: 'void' }, | ||
| }, | ||
| { | ||
| name: 'selectOption', | ||
|
|
@@ -149,7 +138,7 @@ describe('Generate documentation', () => { | |
| defaultValue: '1', | ||
| }, | ||
| ], | ||
| returnType: { name: 'void', type: 'intrinsic' }, | ||
| returnType: { name: 'void' }, | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import { documentTestUtils } from '../../src/test-utils'; | ||
| import { documentTestUtilsNew, TestUtilsDocumenterOptions } from '../../src/test-utils-new'; | ||
| import { TestUtilsDoc } from '../../src/test-utils/interfaces'; | ||
|
|
||
| export function buildTestUtilsProject(name: string, testGlob?: string): TestUtilsDoc[] { | ||
| return documentTestUtils( | ||
| { | ||
| tsconfig: require.resolve(`../../fixtures/test-utils/${name}/tsconfig.json`), | ||
| }, | ||
| testGlob || `fixtures/test-utils/${name}/**/*` | ||
| ); | ||
| export function buildTestUtilsProject( | ||
| name: string, | ||
| configOverrides?: Partial<TestUtilsDocumenterOptions> | ||
| ): TestUtilsDoc[] { | ||
| return documentTestUtilsNew({ | ||
| tsconfigPath: require.resolve(`../../fixtures/test-utils/${name}/tsconfig.json`), | ||
| domUtilsRoot: `fixtures/test-utils/${name}/index.ts`, | ||
| selectorsUtilsRoot: `fixtures/test-utils/${name}/index.ts`, | ||
| ...configOverrides, | ||
| }).domDefinitions; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,24 +5,22 @@ import { buildTestUtilsProject } from './test-helpers'; | |
|
|
||
| describe('documentTestUtils throws error for ', () => { | ||
| test('failing project generation because of invalid config', () => { | ||
| expect(() => buildTestUtilsProject('errors-config')).toThrow('Errors during parsing configuration'); | ||
| expect(() => buildTestUtilsProject('errors-config')).toThrow('Failed to parse tsconfig.json'); | ||
| }); | ||
|
|
||
| test('failing project generation because of faulty project files', () => { | ||
| expect(() => buildTestUtilsProject('errors-types')).toThrow('Project generation failed'); | ||
| expect(() => buildTestUtilsProject('errors-types')).toThrow('Compilation failed'); | ||
| }); | ||
|
|
||
| test('having no input files because of the config', () => { | ||
| expect(() => buildTestUtilsProject('errors-empty')).toThrow('Errors during parsing configuration'); | ||
| expect(() => buildTestUtilsProject('errors-empty')).toThrow('Failed to parse tsconfig.json'); | ||
| }); | ||
|
|
||
| test('having no input files because of a non-matching glob', () => { | ||
| expect(() => buildTestUtilsProject('simple', 'thisGlobWontMatchAnything')).toThrow('No input files to convert'); | ||
| expect(() => | ||
| buildTestUtilsProject('simple', { | ||
| domUtilsRoot: 'fixtures/does-not-exist/index.ts', | ||
| }) | ||
| ).toThrow(/File '.*fixtures\/does-not-exist\/index.ts' not found/); | ||
| }); | ||
| }); | ||
|
|
||
| test('glob works', () => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed anymore, because the new version is based on exports from the main file |
||
| const results = buildTestUtilsProject('glob-test', '**/a.ts'); | ||
| expect(results.length).toBe(1); | ||
| expect(results[0].name).toBe('A'); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old code is still there for compatibility, but the tests already use the new implementation.
Ignoring the old code from the coverage, because it will be removed soon anyway