Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codecov.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignore:
# deprecated code
- 'src/test-utils/**'
Copy link
Member Author

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

3 changes: 0 additions & 3 deletions fixtures/test-utils/glob-test/a.ts

This file was deleted.

3 changes: 0 additions & 3 deletions fixtures/test-utils/glob-test/b.ts

This file was deleted.

4 changes: 0 additions & 4 deletions fixtures/test-utils/glob-test/tsconfig.json

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"components",
"schema",
"test-utils",
"test-utils-new",
"*.js",
"*.d.ts"
],
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
export * from './components/interfaces';
export { writeComponentsDocumentation } from './components';
export { documentTestUtils } from './test-utils';
export { writeTestUtilsDocumentation } from './test-utils-new';
82 changes: 82 additions & 0 deletions src/test-utils-new/extractor.ts
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
)
) {
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;
}
62 changes: 62 additions & 0 deletions src/test-utils-new/index.ts
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;
`;
fs.writeFileSync(pathe.join(outDir, 'dom.d.ts'), dtsTemplate);
fs.writeFileSync(pathe.join(outDir, 'selectors.d.ts'), dtsTemplate);
}
30 changes: 30 additions & 0 deletions src/test-utils-new/interfaces.ts
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>;
}
6 changes: 2 additions & 4 deletions test/test-utils/__snapshots__/doc-generation.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ exports[`Generate documentation > For simple cases 1`] = `
[
{
"defaultValue": undefined,
"description": "

",
"description": undefined,
"flags": {
"isOptional": false,
},
Expand All @@ -25,7 +23,7 @@ exports[`Generate documentation > deal with more complex types 1`] = `
"isOptional": false,
},
"name": "all",
"typeName": "Array",
"typeName": "Array<HTMLElement>",
},
]
`;
39 changes: 14 additions & 25 deletions test/test-utils/doc-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand All @@ -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();
Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The 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();
Expand Down Expand Up @@ -121,11 +109,12 @@ describe('Generate documentation', () => {
parameters: [
{
name: 'order',
typeName: '"first" | "last"',
Copy link
Member Author

Choose a reason for hiding this comment

The 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',
Expand All @@ -137,7 +126,7 @@ describe('Generate documentation', () => {
defaultValue: 'false',
},
],
returnType: { name: 'void', type: 'intrinsic' },
returnType: { name: 'void' },
},
{
name: 'selectOption',
Expand All @@ -149,7 +138,7 @@ describe('Generate documentation', () => {
defaultValue: '1',
},
],
returnType: { name: 'void', type: 'intrinsic' },
returnType: { name: 'void' },
},
]);
});
Expand Down
19 changes: 11 additions & 8 deletions test/test-utils/test-helpers.ts
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;
}
18 changes: 8 additions & 10 deletions test/test-utils/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The 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');
});
Loading
Loading