diff --git a/package.json b/package.json index 049c9ed37..091b57c61 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,12 @@ "build:transformer:definitelyTyped": "webpack --config config/modules/definitelyTypedTransformer/webpack.functions.js && webpack --config config/modules/definitelyTypedTransformer/webpack.js", "build:transformer:definitelyTyped:debug": "cross-env DEBUG=true webpack --config config/modules/definitelyTypedTransformer/webpack.functions.js && cross-env DEBUG=true webpack --config config/modules/definitelyTypedTransformer/webpack.js", "build:playground": "ttsc --project ./test/playground/tsconfig.build.json", - "test": "npm run test:transformer && npm run test:noTransformer && npm run test:framework:context && npm run test:framework && npm run test:frameworkDeprecated && npm run test:registerMock && npm run test:features && npm run test:filesFilter && npm run test:logs && npm run test:unit", + "test": "npm run test:transformer && npm run test:noTransformer && npm run test:framework:context && npm run test:framework && npm run test:frameworkDeprecated && npm run test:registerMock && npm run test:createHydratedMock && npm run test:features && npm run test:filesFilter && npm run test:logs && npm run test:unit", "test:unit": "cross-env JASMINE_CONFIG=./test/unit/jasmine.json TSCONFIG=./test/tsconfig.json npm run test:common", "test:transformer": "cross-env JASMINE_CONFIG=./test/transformer/jasmine.json TSCONFIG=./test/transformer/tsconfig.json npm run test:common", "test:noTransformer": "cross-env JASMINE_CONFIG=./test/noTransformer/jasmine.json TSCONFIG=./test/tsconfig.json npm run test:common", "test:registerMock": "cross-env JASMINE_CONFIG=./test/registerMock/jasmine.json TSCONFIG=./test/registerMock/tsconfig.json npm run test:common", + "test:createHydratedMock": "cross-env JASMINE_CONFIG=./test/createHydratedMock/jasmine.json TSCONFIG=./test/createHydratedMock/tsconfig.json npm run test:common", "test:framework:context": "cross-env JASMINE_CONFIG=./test/frameworkContext/jasmine.json TSCONFIG=./test/frameworkContext/tsconfig.json npm run test:common", "test:frameworkDeprecated": "cross-env JASMINE_CONFIG=./test/frameworkContext/jasmineDeprecated.json TSCONFIG=./test/frameworkContext/tsconfig.json npm run test:common", "test:framework": "cross-env JASMINE_CONFIG=./test/framework/jasmine.json TSCONFIG=./test/framework/tsconfig.json npm run test:common", diff --git a/src/create-hydrated-mock.ts b/src/create-hydrated-mock.ts new file mode 100644 index 000000000..f15043f2b --- /dev/null +++ b/src/create-hydrated-mock.ts @@ -0,0 +1,9 @@ +import { NoTransformerError } from './errors/no-transformer.error'; + +import { PartialDeep } from './partial/partial'; + +export function createHydratedMock( + _values?: PartialDeep +): T { + throw new Error(NoTransformerError); +} diff --git a/src/index.ts b/src/index.ts index 68a88e0f2..971acc694 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export { createMock } from './create-mock'; export { createMockList } from './create-mock-list'; +export { createHydratedMock } from './create-hydrated-mock'; export { registerMock } from './register-mock'; diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index 5c8889353..6c355100f 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -44,7 +44,7 @@ export function GetMockPropertiesFromDeclarations( return false; } - if (member.questionToken) { + if (member.questionToken && !scope.hydrated) { return false; } diff --git a/src/transformer/descriptor/typeParameter/typeParameter.ts b/src/transformer/descriptor/typeParameter/typeParameter.ts index 86b6bc179..c1bac4c9b 100644 --- a/src/transformer/descriptor/typeParameter/typeParameter.ts +++ b/src/transformer/descriptor/typeParameter/typeParameter.ts @@ -33,8 +33,9 @@ export function GetTypeParameterDescriptor( ); } - const genericKey: string = MockDefiner.instance.getDeclarationKeyMap( - typeDeclaration + const genericKey: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + typeDeclaration, + scope ); return createFunctionToAccessToGenericValue( diff --git a/src/transformer/descriptor/typeQuery/typeQuery.ts b/src/transformer/descriptor/typeQuery/typeQuery.ts index a9f420dd2..974103fe6 100644 --- a/src/transformer/descriptor/typeQuery/typeQuery.ts +++ b/src/transformer/descriptor/typeQuery/typeQuery.ts @@ -80,7 +80,10 @@ export function GetTypeQueryDescriptorFromDeclaration( // TODO: Use following two lines when issue #17552 on typescript github is resolved (https://github.com/microsoft/TypeScript/issues/17552) // TheNewEmitResolver.ensureEmitOf(GetImportDeclarationOf(node.eprName as ts.Identifier); // return node.exprName as ts.Identifier; - return GetMockFactoryCallTypeofEnum(declaration as ts.EnumDeclaration); + return GetMockFactoryCallTypeofEnum( + declaration as ts.EnumDeclaration, + scope + ); case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.MethodSignature: return GetMethodDeclarationDescriptor( diff --git a/src/transformer/descriptor/typeReference/typeReference.ts b/src/transformer/descriptor/typeReference/typeReference.ts index c9779b88d..486919686 100644 --- a/src/transformer/descriptor/typeReference/typeReference.ts +++ b/src/transformer/descriptor/typeReference/typeReference.ts @@ -21,8 +21,8 @@ export function GetTypeReferenceDescriptor( node.typeName ); - if (MockDefiner.instance.hasMockForDeclaration(declaration)) { - return GetMockFactoryCall(node, scope); + if (MockDefiner.instance.hasMockForDeclaration(declaration, scope)) { + return GetMockFactoryCall(node, declaration, scope); } if (IsTypescriptType(declaration)) { @@ -30,7 +30,7 @@ export function GetTypeReferenceDescriptor( } if (isTypeReferenceReusable(declaration)) { - return CreateMockFactory(node, scope); + return CreateMockFactory(node, declaration, scope); } return GetDescriptor(declaration, scope); diff --git a/src/transformer/descriptor/union/union.ts b/src/transformer/descriptor/union/union.ts index 0bde76d7c..4aa1c40a7 100644 --- a/src/transformer/descriptor/union/union.ts +++ b/src/transformer/descriptor/union/union.ts @@ -10,6 +10,18 @@ export function GetUnionDescriptor( ): ts.Expression { const findNodes: ts.Node[] = GetTypes(node.types, scope); + if (scope.hydrated) { + const removeUndefinedNodes: ts.Node[] = findNodes.filter( + (typeNode: ts.TypeNode) => !isNotDefinedType(typeNode) + ); + + if (removeUndefinedNodes.length) { + return GetDescriptor(removeUndefinedNodes[0], scope); + } + + return GetUndefinedDescriptor(); + } + const notDefinedType: ts.Node[] = findNodes.filter((typeNode: ts.TypeNode) => isNotDefinedType(typeNode) ); diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index b24737f92..335354b0f 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -132,10 +132,13 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { genericNode.typeName ); + const typeParameterDeclarationKey: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + typeParameterDeclaration, + scope + ); + const isExtendingItself: boolean = - MockDefiner.instance.getDeclarationKeyMap( - typeParameterDeclaration - ) === declarationKey; + typeParameterDeclarationKey === declarationKey; if (isExtendingItself) { // FIXME: Currently, circular generics aren't supported. See // https://github.com/Typescript-TDD/ts-auto-mock/pull/312 for more diff --git a/src/transformer/mock/mock.ts b/src/transformer/mock/mock.ts index 9443897f1..0d8579f18 100644 --- a/src/transformer/mock/mock.ts +++ b/src/transformer/mock/mock.ts @@ -18,6 +18,12 @@ function getMockExpression(nodeToMock: ts.TypeNode): ts.Expression { return GetDescriptor(nodeToMock, new Scope()); } +function getMockHydratedExpression(nodeToMock: ts.TypeNode): ts.Expression { + const scope: Scope = new Scope(); + scope.hydrated = true; + return GetDescriptor(nodeToMock, scope); +} + function hasDefaultValues(node: ts.CallExpression): boolean { return !!node.arguments.length && !!node.arguments[0]; } @@ -41,6 +47,21 @@ export function getMock( return mockExpression; } +export function getHydratedMock( + nodeToMock: ts.TypeNode, + node: ts.CallExpression +): ts.Expression { + SetCurrentCreateMock(node); + + const mockExpression: ts.Expression = getMockHydratedExpression(nodeToMock); + + if (hasDefaultValues(node)) { + return getMockMergeExpression(mockExpression, node.arguments[0]); + } + + return mockExpression; +} + export function getMockForList( nodeToMock: ts.TypeNode, node: ts.CallExpression diff --git a/src/transformer/mockDefiner/mockDefiner.ts b/src/transformer/mockDefiner/mockDefiner.ts index 8fec264fe..f33d2a546 100644 --- a/src/transformer/mockDefiner/mockDefiner.ts +++ b/src/transformer/mockDefiner/mockDefiner.ts @@ -7,7 +7,6 @@ import { GetDescriptor } from '../descriptor/descriptor'; import { GetProperties } from '../descriptor/properties/properties'; import { GetTypeofEnumDescriptor } from '../descriptor/typeQuery/enumTypeQuery'; import { TypescriptCreator } from '../helper/creator'; -import { createImportOnIdentifier } from '../helper/import'; import { MockIdentifierGenericParameter, MockIdentifierGenericParameterValue, @@ -18,8 +17,7 @@ import { DeclarationCache } from './cache/declarationCache'; import { DeclarationListCache } from './cache/declarationListCache'; import { FactoryUniqueName, PossibleDeclaration } from './factoryUniqueName'; import { ModuleName } from './modules/moduleName'; -import { ModuleNameIdentifier } from './modules/moduleNameIdentifier'; -import { ModulesImportUrl } from './modules/modulesImportUrl'; +import { ModuleImportIdentifierPerFile } from './modules/moduleImportIdentifierPerFile'; interface FactoryRegistrationPerFile { [key: string]: Array<{ @@ -36,17 +34,15 @@ interface FactoryIntersectionRegistrationPerFile { } export class MockDefiner { - private _neededImportIdentifierPerFile: { - [key: string]: Array; - } = {}; - private _internalModuleImportIdentifierPerFile: { - [key: string]: { [key in ModuleName]: ts.Identifier }; - } = {}; + private _moduleImportIdentifierPerFile: ModuleImportIdentifierPerFile; private _factoryRegistrationsPerFile: FactoryRegistrationPerFile = {}; + private _hydratedFactoryRegistrationsPerFile: FactoryRegistrationPerFile = {}; private _factoryIntersectionsRegistrationsPerFile: FactoryIntersectionRegistrationPerFile = {}; private _factoryCache: DeclarationCache; + private _hydratedFactoryCache: DeclarationCache; private _registerMockFactoryCache: DeclarationCache; private _declarationCache: DeclarationCache; + private _hydratedDeclarationCache: DeclarationCache; private _factoryIntersectionCache: DeclarationListCache; private _fileName: string; private _factoryUniqueName: FactoryUniqueName; @@ -55,9 +51,12 @@ export class MockDefiner { private constructor() { this._factoryCache = new DeclarationCache(); this._declarationCache = new DeclarationCache(); + this._hydratedDeclarationCache = new DeclarationCache(); + this._hydratedFactoryCache = new DeclarationCache(); this._factoryIntersectionCache = new DeclarationListCache(); this._factoryUniqueName = new FactoryUniqueName(); this._registerMockFactoryCache = new DeclarationCache(); + this._moduleImportIdentifierPerFile = new ModuleImportIdentifierPerFile(); this._cacheEnabled = GetTsAutoMockCacheOptions(); } @@ -74,29 +73,11 @@ export class MockDefiner { } public setTsAutoMockImportIdentifier(): void { - if (this._internalModuleImportIdentifierPerFile[this._fileName]) { + if (this._moduleImportIdentifierPerFile.has(this._fileName)) { return; } - this._internalModuleImportIdentifierPerFile[this._fileName] = { - [ModuleName.Extension]: PrivateIdentifier(ModuleName.Extension), - [ModuleName.Merge]: PrivateIdentifier(ModuleName.Merge), - [ModuleName.Repository]: PrivateIdentifier(ModuleName.Repository), - [ModuleName.Random]: PrivateIdentifier(ModuleName.Random), - }; - - this._neededImportIdentifierPerFile[this._fileName] = - this._neededImportIdentifierPerFile[this._fileName] || []; - - Array.prototype.push.apply( - this._neededImportIdentifierPerFile[this._fileName], - Object.keys(ModulesImportUrl).map((key: ModuleName) => ({ - moduleUrl: ModulesImportUrl[key], - identifier: this._internalModuleImportIdentifierPerFile[this._fileName][ - key - ], - })) - ); + this._moduleImportIdentifierPerFile.set(this._fileName); } public getCurrentModuleIdentifier(module: ModuleName): ts.Identifier { @@ -107,6 +88,7 @@ export class MockDefiner { return [ ...this._getImportsToAddInFile(sourceFile), ...this._getExportsToAddInFile(sourceFile), + ...this._getHydratedExportsToAddInFile(sourceFile), ...this._getExportsIntersectionToAddInFile(sourceFile), ]; } @@ -114,56 +96,83 @@ export class MockDefiner { public initFile(sourceFile: ts.SourceFile): void { if (!this._cacheEnabled) { this._factoryCache = new DeclarationCache(); + this._hydratedFactoryCache = new DeclarationCache(); this._declarationCache = new DeclarationCache(); + this._hydratedDeclarationCache = new DeclarationCache(); this._factoryIntersectionCache = new DeclarationListCache(); } this._factoryRegistrationsPerFile[sourceFile.fileName] = []; + this._hydratedFactoryRegistrationsPerFile[sourceFile.fileName] = []; this._factoryIntersectionsRegistrationsPerFile[sourceFile.fileName] = []; } - public createMockFactory(declaration: ts.Declaration): void { + public createMockFactory(declaration: ts.Declaration, scope: Scope): void { const thisFileName: string = this._fileName; - const key: string = this.getDeclarationKeyMap(declaration); + if (scope.hydrated) { + const key: string = this._getHydratedDeclarationKeyMap(declaration); + this._hydratedFactoryCache.set(declaration, key); + this._hydratedFactoryRegistrationsPerFile[thisFileName] = + this._hydratedFactoryRegistrationsPerFile[thisFileName] || []; - this._factoryCache.set(declaration, key); + const descriptor: ts.Expression = GetDescriptor( + declaration, + Scope.fromScope(scope, key) + ); - this._factoryRegistrationsPerFile[thisFileName] = - this._factoryRegistrationsPerFile[thisFileName] || []; + const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter(); - const descriptor: ts.Expression = GetDescriptor( - declaration, - new Scope(key) - ); + const factory: ts.FunctionExpression = TypescriptCreator.createFunctionExpressionReturn( + descriptor, + [mockGenericParameter] + ); + this._hydratedFactoryRegistrationsPerFile[thisFileName].push({ + key: declaration, + factory, + }); + } else { + const key: string = this._getDeclarationKeyMap(declaration); + this._factoryCache.set(declaration, key); + this._factoryRegistrationsPerFile[thisFileName] = + this._factoryRegistrationsPerFile[thisFileName] || []; - const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter(); + const descriptor: ts.Expression = GetDescriptor( + declaration, + Scope.fromScope(scope, key) + ); - const factory: ts.FunctionExpression = TypescriptCreator.createFunctionExpressionReturn( - descriptor, - [mockGenericParameter] - ); + const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter(); - this._factoryRegistrationsPerFile[thisFileName].push({ - key: declaration, - factory, - }); + const factory: ts.FunctionExpression = TypescriptCreator.createFunctionExpressionReturn( + descriptor, + [mockGenericParameter] + ); + + this._factoryRegistrationsPerFile[thisFileName].push({ + key: declaration, + factory, + }); + } } public getMockFactoryTypeofEnum( - declaration: ts.EnumDeclaration + declaration: ts.EnumDeclaration, + scope: Scope ): ts.Expression { - const key: string = this._getMockFactoryIdForTypeofEnum(declaration); + const key: string = this._getMockFactoryIdForTypeofEnum(declaration, scope); return this.getMockFactoryByKey(key); } public getMockFactoryIntersection( declarations: ts.Declaration[], - type: ts.IntersectionTypeNode + type: ts.IntersectionTypeNode, + scope: Scope ): ts.Expression { const key: string = this._getMockFactoryIdForIntersections( declarations, - type + type, + scope ); return this.getMockFactoryByKey(key); @@ -175,42 +184,94 @@ export class MockDefiner { return this._getCallGetFactory(key); } - public getDeclarationKeyMap(declaration: ts.Declaration): string { - if (!this._declarationCache.has(declaration)) { - this._declarationCache.set( - declaration, - this._factoryUniqueName.createForDeclaration( - declaration as PossibleDeclaration - ) - ); + public getDeclarationKeyMapBasedOnScope( + declaration: ts.Declaration, + scope: Scope + ): string { + if (scope.hydrated) { + return this._getHydratedDeclarationKeyMap(declaration); } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this._declarationCache.get(declaration)!; + return this._getDeclarationKeyMap(declaration); } public registerMockFor( declaration: ts.Declaration, factory: ts.FunctionExpression ): ts.Node { - const key: string = this.getDeclarationKeyMap(declaration); + const key: string = this._getDeclarationKeyMap(declaration); + const hydratedKey: string = this._getHydratedDeclarationKeyMap(declaration); this._registerMockFactoryCache.set(declaration, key); - return this._getCallRegisterMock( - this._fileName, - key, - this._wrapRegisterMockFactory(factory) + return TypescriptCreator.createIIFE( + ts.createBlock( + [ + ts.createExpressionStatement( + this._getCallRegisterMock( + this._fileName, + hydratedKey, + this._wrapRegisterMockFactory(factory) + ) + ), + ts.createExpressionStatement( + this._getCallRegisterMock( + this._fileName, + key, + this._wrapRegisterMockFactory(factory) + ) + ), + ], + true + ) ); } - public hasMockForDeclaration(declaration: ts.Declaration): boolean { + public hasMockForDeclaration( + declaration: ts.Declaration, + scope: Scope + ): boolean { + if (scope.hydrated) { + return ( + this._hydratedFactoryCache.has(declaration) || + this._registerMockFactoryCache.has(declaration) + ); + } + return ( this._factoryCache.has(declaration) || this._registerMockFactoryCache.has(declaration) ); } + private _getDeclarationKeyMap(declaration: ts.Declaration): string { + if (!this._declarationCache.has(declaration)) { + this._declarationCache.set( + declaration, + this._factoryUniqueName.createForDeclaration( + declaration as PossibleDeclaration + ) + ); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this._declarationCache.get(declaration)!; + } + + private _getHydratedDeclarationKeyMap(declaration: ts.Declaration): string { + if (!this._hydratedDeclarationCache.has(declaration)) { + this._hydratedDeclarationCache.set( + declaration, + this._factoryUniqueName.createForDeclaration( + declaration as PossibleDeclaration + ) + ); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this._hydratedDeclarationCache.get(declaration)!; + } + private _mockRepositoryAccess(filename: string): ts.Expression { const repository: ts.Identifier = this._getModuleIdentifier( filename, @@ -227,10 +288,11 @@ export class MockDefiner { fileName: string, module: ModuleName ): ts.Identifier { - return this._internalModuleImportIdentifierPerFile[fileName][module]; + return this._moduleImportIdentifierPerFile.getModule(fileName, module); } private _getMockFactoryIdForTypeofEnum( - declaration: ts.EnumDeclaration + declaration: ts.EnumDeclaration, + scope: Scope ): string { const thisFileName: string = this._fileName; @@ -241,7 +303,10 @@ export class MockDefiner { return cachedFactory; } - const key: string = this.getDeclarationKeyMap(declaration); + const key: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + declaration, + scope + ); this._factoryCache.set(declaration, key); @@ -260,7 +325,8 @@ export class MockDefiner { private _getMockFactoryIdForIntersections( declarations: ts.Declaration[], - intersectionTypeNode: ts.IntersectionTypeNode + intersectionTypeNode: ts.IntersectionTypeNode, + scope: Scope ): string { const thisFileName: string = this._fileName; @@ -280,7 +346,7 @@ export class MockDefiner { const descriptor: ts.Expression = GetProperties( intersectionTypeNode, - new Scope(key) + Scope.fromScope(scope, key) ); const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter(); @@ -299,15 +365,8 @@ export class MockDefiner { } private _getImportsToAddInFile(sourceFile: ts.SourceFile): ts.Statement[] { - if (this._neededImportIdentifierPerFile[sourceFile.fileName]) { - return this._neededImportIdentifierPerFile[ - sourceFile.fileName - ].map((moduleIdentifier: ModuleNameIdentifier) => - createImportOnIdentifier( - moduleIdentifier.moduleUrl, - moduleIdentifier.identifier - ) - ); + if (this._moduleImportIdentifierPerFile.has(sourceFile.fileName)) { + return this._moduleImportIdentifierPerFile.get(sourceFile.fileName); } return []; @@ -335,6 +394,30 @@ export class MockDefiner { return []; } + private _getHydratedExportsToAddInFile( + sourceFile: ts.SourceFile + ): ts.Statement[] { + if (this._hydratedFactoryRegistrationsPerFile[sourceFile.fileName]) { + return this._hydratedFactoryRegistrationsPerFile[sourceFile.fileName].map( + (reg: { key: ts.Declaration; factory: ts.Expression }) => { + // NOTE: this._hydratedFactoryRegistrationsPerFile and this._hydratedFactoryCache are + // populated in the same routine and if the former is defined the + // latter will be too! + // eslint-disable-next-line + const key: string = this._hydratedFactoryCache.get(reg.key)!; + + return this._createRegistration( + sourceFile.fileName, + key, + reg.factory + ); + } + ); + } + + return []; + } + private _getExportsIntersectionToAddInFile( sourceFile: ts.SourceFile ): ts.Statement[] { diff --git a/src/transformer/mockDefiner/modules/moduleImportIdentifierPerFile.ts b/src/transformer/mockDefiner/modules/moduleImportIdentifierPerFile.ts new file mode 100644 index 000000000..3d66a7f5b --- /dev/null +++ b/src/transformer/mockDefiner/modules/moduleImportIdentifierPerFile.ts @@ -0,0 +1,51 @@ +import * as ts from 'typescript'; +import { createImportOnIdentifier } from '../../helper/import'; +import { PrivateIdentifier } from '../../privateIdentifier/privateIdentifier'; +import { ModuleNameIdentifier } from './moduleNameIdentifier'; +import { ModulesImportUrl } from './modulesImportUrl'; +import { ModuleName } from './moduleName'; + +export class ModuleImportIdentifierPerFile { + private _modules: { + [fileName: string]: Array; + } = {}; + + private _modulesNameIdentifierPerFile: { + [fileName: string]: { [key in ModuleName]: ts.Identifier }; + } = {}; + + public has(fileName: string): boolean { + return !!this._modules[fileName]; + } + + public set(fileName: string): void { + this._modulesNameIdentifierPerFile[fileName] = { + [ModuleName.Extension]: PrivateIdentifier(ModuleName.Extension), + [ModuleName.Merge]: PrivateIdentifier(ModuleName.Merge), + [ModuleName.Repository]: PrivateIdentifier(ModuleName.Repository), + [ModuleName.Random]: PrivateIdentifier(ModuleName.Random), + }; + + this._modules[fileName] = Object.keys(ModulesImportUrl).map( + (key: ModuleName) => ({ + moduleUrl: ModulesImportUrl[key], + identifier: this._modulesNameIdentifierPerFile[fileName][key], + }) + ); + } + + public getModule(fileName: string, moduleName: ModuleName): ts.Identifier { + return this._modulesNameIdentifierPerFile[fileName][moduleName]; + } + + public get(fileName: string): ts.Statement[] { + return this._modules[ + fileName + ].map((moduleIdentifier: ModuleNameIdentifier) => + createImportOnIdentifier( + moduleIdentifier.moduleUrl, + moduleIdentifier.identifier + ) + ); + } +} diff --git a/src/transformer/mockFactoryCall/mockFactoryCall.ts b/src/transformer/mockFactoryCall/mockFactoryCall.ts index a0d31eb25..baba17383 100644 --- a/src/transformer/mockFactoryCall/mockFactoryCall.ts +++ b/src/transformer/mockFactoryCall/mockFactoryCall.ts @@ -13,23 +13,18 @@ import { TypescriptCreator } from '../helper/creator'; export function GetMockFactoryCall( typeReferenceNode: ts.TypeReferenceNode, + declaration: ts.Declaration, scope: Scope ): ts.Expression { - const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode( - typeReferenceNode.typeName - ); - return getDeclarationMockFactoryCall(declaration, typeReferenceNode, scope); } export function CreateMockFactory( typeReferenceNode: ts.TypeReferenceNode, + declaration: ts.Declaration, scope: Scope ): ts.Expression { - const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode( - typeReferenceNode.typeName - ); - MockDefiner.instance.createMockFactory(declaration); + MockDefiner.instance.createMockFactory(declaration, scope); return getDeclarationMockFactoryCall(declaration, typeReferenceNode, scope); } @@ -47,8 +42,9 @@ export function GetMockFactoryCallIntersection( const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode( type.typeName ); - const declarationKey: string = MockDefiner.instance.getDeclarationKeyMap( - declaration + const declarationKey: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + declaration, + scope ); genericDeclaration.addFromTypeReferenceNode(type, declarationKey); @@ -56,7 +52,8 @@ export function GetMockFactoryCallIntersection( addFromDeclarationExtensions( declaration as GenericDeclarationSupported, declarationKey, - genericDeclaration + genericDeclaration, + scope ); return declaration; @@ -67,7 +64,8 @@ export function GetMockFactoryCallIntersection( const genericsParametersExpression: ts.ObjectLiteralExpression[] = genericDeclaration.getExpressionForAllGenerics(); const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryIntersection( declarations, - intersection + intersection, + scope ); return TypescriptCreator.createCall(mockFactoryCall, [ @@ -76,10 +74,12 @@ export function GetMockFactoryCallIntersection( } export function GetMockFactoryCallTypeofEnum( - declaration: ts.EnumDeclaration + declaration: ts.EnumDeclaration, + scope: Scope ): ts.Expression { const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryTypeofEnum( - declaration + declaration, + scope ); return TypescriptCreator.createCall(mockFactoryCall, []); @@ -100,9 +100,10 @@ function getDeclarationMockFactoryCall( typeReferenceNode: ts.TypeReferenceNode, scope: Scope ): ts.Expression { - const declarationKey: - | string - | undefined = MockDefiner.instance.getDeclarationKeyMap(declaration); + const declarationKey: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + declaration, + scope + ); if (!declarationKey) { throw new Error( @@ -123,7 +124,8 @@ function getDeclarationMockFactoryCall( addFromDeclarationExtensions( declaration as GenericDeclarationSupported, declarationKey, - genericDeclaration + genericDeclaration, + scope ); const genericsParametersExpression: ts.ObjectLiteralExpression[] = genericDeclaration.getExpressionForAllGenerics(); @@ -136,7 +138,8 @@ function getDeclarationMockFactoryCall( function addFromDeclarationExtensions( declaration: GenericDeclarationSupported, declarationKey: string, - genericDeclaration: IGenericDeclaration + genericDeclaration: IGenericDeclaration, + scope: Scope ): void { if (declaration.heritageClauses) { declaration.heritageClauses.forEach((clause: ts.HeritageClause) => { @@ -149,18 +152,11 @@ function addFromDeclarationExtensions( extension.expression ); - const extensionDeclarationKey: - | string - | undefined = MockDefiner.instance.getDeclarationKeyMap( - extensionDeclaration + const extensionDeclarationKey: string = MockDefiner.instance.getDeclarationKeyMapBasedOnScope( + extensionDeclaration, + scope ); - if (!extensionDeclarationKey) { - throw new Error( - `Failed to look up declaration key in MockDefiner for \`${extensionDeclaration.getText()}'.` - ); - } - genericDeclaration.addFromDeclarationExtension( declarationKey, extensionDeclaration as GenericDeclarationSupported, @@ -171,7 +167,8 @@ function addFromDeclarationExtensions( addFromDeclarationExtensions( extensionDeclaration as GenericDeclarationSupported, extensionDeclarationKey, - genericDeclaration + genericDeclaration, + scope ); }); }); diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index d7dc55e21..de8fb968a 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -4,13 +4,28 @@ export type InterfaceOrClassDeclaration = | ts.InterfaceDeclaration | ts.ClassDeclaration; export class Scope { + private _hydrated: boolean; constructor(currentMockKey?: string) { this._currentMockKey = currentMockKey; } + public static fromScope(scope: Scope, currentMockKey: string): Scope { + const newScope: Scope = new Scope(currentMockKey); + newScope.hydrated = scope.hydrated; + return newScope; + } + private readonly _currentMockKey: string | undefined; public get currentMockKey(): string | undefined { return this._currentMockKey; } + + public get hydrated(): boolean { + return this._hydrated; + } + + public set hydrated(hydrated: boolean) { + this._hydrated = hydrated; + } } diff --git a/src/transformer/transformer.ts b/src/transformer/transformer.ts index f09e27feb..04a2fe165 100644 --- a/src/transformer/transformer.ts +++ b/src/transformer/transformer.ts @@ -1,7 +1,12 @@ import * as ts from 'typescript'; import { TsAutoMockOptions } from '../options/options'; import { CustomFunction } from './matcher/matcher'; -import { getMock, getMockForList, storeRegisterMock } from './mock/mock'; +import { + getHydratedMock, + getMock, + getMockForList, + storeRegisterMock, +} from './mock/mock'; import { baseTransformer } from './base/base'; const customFunctions: CustomFunction[] = [ @@ -17,6 +22,10 @@ const customFunctions: CustomFunction[] = [ sourceDts: 'register-mock.d.ts', sourceUrl: '../register-mock.d.ts', }, + { + sourceDts: 'create-hydrated-mock.d.ts', + sourceUrl: '../create-hydrated-mock.d.ts', + }, ]; const transformer: ( @@ -39,6 +48,10 @@ function visitNode( return getMock(nodeToMock, node); } + if (isCreateHydratedMock(declaration)) { + return getHydratedMock(nodeToMock, node); + } + if (isCreateMockList(declaration)) { return getMockForList(nodeToMock, node); } @@ -54,6 +67,10 @@ function isCreateMock(declaration: ts.FunctionDeclaration): boolean { return declaration.name?.getText() === 'createMock'; } +function isCreateHydratedMock(declaration: ts.FunctionDeclaration): boolean { + return declaration.name?.getText() === 'createHydratedMock'; +} + function isCreateMockList(declaration: ts.FunctionDeclaration): boolean { return declaration.name?.getText() === 'createMockList'; } diff --git a/test/createHydratedMock/create-hydrated-mock.test.ts b/test/createHydratedMock/create-hydrated-mock.test.ts new file mode 100644 index 000000000..539af4686 --- /dev/null +++ b/test/createHydratedMock/create-hydrated-mock.test.ts @@ -0,0 +1,232 @@ +import { createHydratedMock, createMock, registerMock } from 'ts-auto-mock'; + +describe('create-hydrated-mock', () => { + describe('for not optional properties', () => { + it('should treat them as non optional', () => { + interface Interface { + notRequired?: string; + } + + const mock: Interface = createHydratedMock(); + expect(mock.notRequired).toBe(''); + }); + }); + + describe('for union types', () => { + it('should ignore not defined types', () => { + interface Interface { + method(): string | undefined; + method2(): undefined | string; + method3(): string | void; + method4(): void | string; + property: string | undefined; + property2: undefined | string; + property3: string | void; + property4: void | string; + } + + const mock: Interface = createHydratedMock(); + expect(mock.method()).toBe(''); + expect(mock.method2()).toBe(''); + expect(mock.method3()).toBe(''); + expect(mock.method4()).toBe(''); + expect(mock.property).toBe(''); + expect(mock.property2).toBe(''); + expect(mock.property3).toBe(''); + expect(mock.property4).toBe(''); + }); + + it('should still return undefined for union types with only undefined types', () => { + interface Interface { + method(): void | undefined; + method2(): undefined | void; + property: void | undefined; + property2: undefined | void; + } + + const mock: Interface = createHydratedMock(); + expect(mock.method()).toBeUndefined(); + expect(mock.method()).toBeUndefined(); + expect(mock.property).toBeUndefined(); + expect(mock.property2).toBeUndefined(); + }); + }); + + describe('when an interface has already been mocked by createMock', () => { + it('should create a different mock with optional properties defined', () => { + interface Interface { + notRequired?: string; + } + + createMock(); + const mock: Interface = createHydratedMock(); + expect(mock.notRequired).toBe(''); + }); + }); + + describe('when an interface has already been mocked by createHydratedMock', () => { + it('should create a different mock for createMock with optional properties undefined', () => { + interface Interface { + notRequired?: string; + } + + createHydratedMock(); + const mock: Interface = createMock(); + expect(mock.notRequired).toBeUndefined(); + }); + }); + + describe('for generics', () => { + it('should use the correct declaration to find the correct generic value', () => { + interface Interface { + notRequired?: T; + } + + const mock: Interface = createHydratedMock>(); + expect(mock.notRequired).toBe(''); + }); + + it('should duplicate the mocks', () => { + interface AnotherInterface { + prop: boolean; + } + interface Interface { + notRequired?: T; + } + + createMock(); + const mock: Interface = createHydratedMock< + Interface + >(); + expect(mock.notRequired?.prop).toBe(false); + }); + + it('should find the correct generic with extensions', () => { + interface A { + a: T; + } + + interface B extends A { + b?: number; + } + + const mock: B = createHydratedMock>(); + expect(mock.b).toBe(0); + expect(mock.a).toBe(''); + }); + + it('should avoid infinite extension', () => { + class ClassWithGenerics { + public a: T; + } + + interface A extends ClassWithGenerics { + b: number; + } + const properties: A = createHydratedMock(); + expect(properties.a).toBeDefined(); + expect(properties.b).toBe(0); + }); + }); + + describe('for registerMock', () => { + it('should return the registered mock when using createMock or createHydratedMock', () => { + interface Interface { + notRequired?: string; + } + + registerMock(() => ({ + notRequired: 'hello-world', + })); + const mock: Interface = createHydratedMock(); + expect(mock.notRequired).toBe('hello-world'); + expect(createMock().notRequired).toBe('hello-world'); + }); + }); + + describe('for type of enum', () => { + it('should mock the properties as it was createMock', () => { + enum Enum { + A, + B = 'some', + } + + const enumMock: typeof Enum = createHydratedMock(); + + expect(enumMock.A).toEqual(0); + expect(enumMock.B).toEqual('some'); + }); + }); + + describe('when mocking different interfaces with same name', () => { + // eslint-disable-next-line @typescript-eslint/typedef + let mock0; + // eslint-disable-next-line @typescript-eslint/typedef + let mock1; + // eslint-disable-next-line @typescript-eslint/typedef + let mock2; + // eslint-disable-next-line @typescript-eslint/typedef + let mock3; + it('should create unique mocks', () => { + interface Interface { + prop?: string; + } + + mock0 = createHydratedMock(); + mock1 = createMock(); + + expect(mock0.prop).toEqual(''); + expect(mock1.prop).toBeUndefined(''); + }); + + it('should create unique mocks', () => { + interface Interface { + prop?: string; + } + + mock2 = createHydratedMock(); + mock3 = createMock(); + + expect(mock2.prop).toEqual(''); + expect(mock3.prop).toBeUndefined(''); + }); + + afterAll(() => { + expect(mock0).not.toBe(mock1); + expect(mock0).not.toBe(mock2); + expect(mock0).not.toBe(mock3); + expect(mock1).not.toBe(mock2); + expect(mock1).not.toBe(mock3); + expect(mock2).not.toBe(mock3); + }); + }); + + describe('for intersections', () => { + interface IntersectionA { + a?: string; + } + + interface IntersectionB { + b?: number; + } + interface Interface { + intersection: IntersectionA & IntersectionB; + anotherIntersection: IntersectionA & { + c?: boolean; + }; + } + + it('should merge all the values', () => { + const properties: Interface = createHydratedMock(); + expect(properties.intersection).toEqual({ + a: '', + b: 0, + }); + + expect(properties.anotherIntersection).toEqual({ + a: '', + c: false, + }); + }); + }); +}); diff --git a/test/createHydratedMock/jasmine.json b/test/createHydratedMock/jasmine.json new file mode 100644 index 000000000..08f1b0a3a --- /dev/null +++ b/test/createHydratedMock/jasmine.json @@ -0,0 +1,5 @@ +{ + "spec_dir": "./test/createHydratedMock", + "spec_files": ["**/*test.ts"], + "helpers": ["../reporter.js"] +} diff --git a/test/createHydratedMock/tsconfig.json b/test/createHydratedMock/tsconfig.json new file mode 100644 index 000000000..0f9dbb933 --- /dev/null +++ b/test/createHydratedMock/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "plugins": [ + { + "transform": "./dist/transformer", + "features": [], + "debug": false, + "cacheBetweenTests": true + } + ] + } +} diff --git a/ui/doczrc.js b/ui/doczrc.js index 8709491c3..b84545d00 100644 --- a/ui/doczrc.js +++ b/ui/doczrc.js @@ -5,23 +5,24 @@ export default { typescript: true, themeConfig: { initialColorMode: 'dark', - showDarkModeSwitch: false + showDarkModeSwitch: false, }, title: 'TS auto mock', base: `/${libName}/`, menu: [ - { name: 'Home'}, - { name: 'Installation'}, - { name: 'Create mock', menu: ['Default']}, - { name: 'Create mock list'}, - { name: 'Register mock'}, - { name: 'Extension'}, - { name: 'Types supported'}, - { name: 'Types not supported'}, - { name: 'Config'}, - { name: 'Performance'}, - { name: 'Definitely Typed'}, - { name: 'Local development'} + { name: 'Home' }, + { name: 'Installation' }, + { name: 'Create mock', menu: ['Default'] }, + { name: 'Create mock list' }, + { name: 'Create hydrated mock' }, + { name: 'Register mock' }, + { name: 'Extension' }, + { name: 'Types supported' }, + { name: 'Types not supported' }, + { name: 'Config' }, + { name: 'Performance' }, + { name: 'Definitely Typed' }, + { name: 'Local development' }, ], - repository: "https://github.com/Typescript-TDD/ts-auto-mock" + repository: 'https://github.com/Typescript-TDD/ts-auto-mock', }; diff --git a/ui/src/views/create-hydrated-mock.mdx b/ui/src/views/create-hydrated-mock.mdx new file mode 100644 index 000000000..c902f6c90 --- /dev/null +++ b/ui/src/views/create-hydrated-mock.mdx @@ -0,0 +1,65 @@ +--- +name: Create hydrated mock +route: /create-hydrated-mock +--- + +# Create hydrated mock + +Do you need to mock **optional** properties or union types that may be undefined?
+Say hello to **createHydratedMock**, it will help you create mocks that will treat optional interfaces as they were not optional + +We currently support optional token (?) and union types that contains undefined. + +## Optional interfaces + +```ts +import { createHydratedMock } from 'ts-auto-mock'; + +interface Person { + id?: string; + details?: { + phone?: number + } +} +const mock = createHydratedMock(); +mock.id // "" +mock.details // { phone: 0 } +``` + +## Union types + +```ts +import { createHydratedMock } from 'ts-auto-mock'; + +interface Person { + getName(): string | undefined; + getSurname(): undefined | string; +} +const mock = createHydratedMock(); +mock.getName() // '' +mock.getSurname() // '' +``` + +--- + +## Note from the team + +We completely understand the need of this functionality, but we would like the usage to be much simpler.
+We would like to use the existing createMock in combination of typescript Required interface + +```ts +import { createMock } from 'ts-auto-mock'; + +interface Person { + id?: string; +} +type Required = { // from typescript lib + [P in keyof T]-?: T[P]; +}; +const mock = createMock>(); +mock.id // "" +``` + +Unfortunately this doesn't work because we don't fully support map types. + +