diff --git a/libs/ng-mocks/src/lib/common/core.helpers.ts b/libs/ng-mocks/src/lib/common/core.helpers.ts index 6000100a25..f1f8d59554 100644 --- a/libs/ng-mocks/src/lib/common/core.helpers.ts +++ b/libs/ng-mocks/src/lib/common/core.helpers.ts @@ -4,6 +4,7 @@ import { getTestBed } from '@angular/core/testing'; import coreDefineProperty from './core.define-property'; import coreReflectJit from './core.reflect.jit'; import { AnyType, Type } from './core.types'; +import funcGetName from './func.get-name'; export const getTestBedInjection = (token: AnyType | InjectionToken): I | undefined => { const testBed: any = getTestBed(); @@ -105,7 +106,7 @@ const extendClassicClass = (base: AnyType): Type => { export const extendClass = (base: AnyType): Type => { const child: Type = extendClassicClass(base); - coreDefineProperty(child, 'name', `MockMiddleware${base.name}`, true); + coreDefineProperty(child, 'name', `MockMiddleware${funcGetName(base)}`, true); const parameters = coreReflectJit().parameters(base); if (parameters.length) { diff --git a/libs/ng-mocks/src/lib/common/decorate.mock.ts b/libs/ng-mocks/src/lib/common/decorate.mock.ts index 8cdafe69d0..67c95dd424 100644 --- a/libs/ng-mocks/src/lib/common/decorate.mock.ts +++ b/libs/ng-mocks/src/lib/common/decorate.mock.ts @@ -1,12 +1,13 @@ import coreDefineProperty from './core.define-property'; import { AnyType } from './core.types'; +import funcGetName from './func.get-name'; import { ngMocksMockConfig } from './mock'; import ngMocksUniverse from './ng-mocks-universe'; export default function (mock: AnyType, source: AnyType, configInput: ngMocksMockConfig = {}): void { coreDefineProperty(mock, 'mockOf', source); - coreDefineProperty(mock, 'nameConstructor', mock.name); - coreDefineProperty(mock, 'name', `MockOf${source.name}`, true); + coreDefineProperty(mock, 'nameConstructor', funcGetName(mock)); + coreDefineProperty(mock, 'name', `MockOf${funcGetName(source)}`, true); const config = ngMocksUniverse.getConfigMock().has(source) ? { ...configInput, diff --git a/libs/ng-mocks/src/lib/common/error.jest-mock.ts b/libs/ng-mocks/src/lib/common/error.jest-mock.ts index 1ff868f2d4..fdfff13268 100644 --- a/libs/ng-mocks/src/lib/common/error.jest-mock.ts +++ b/libs/ng-mocks/src/lib/common/error.jest-mock.ts @@ -1,12 +1,15 @@ +import funcGetName from './func.get-name'; import funcIsJestMock from './func.is-jest-mock'; export default (def: any): void => { if (funcIsJestMock(def)) { throw new Error( [ - `ng-mocks got ${def.name} which has been already mocked by jest.mock().`, + `ng-mocks got ${funcGetName(def)} which has been already mocked by jest.mock().`, 'It is not possible to produce correct mocks for it, because jest.mock() removes Angular decorators.', - `To fix this, please avoid jest.mock() on the file which exports ${def.name} or add jest.dontMock() on it.`, + `To fix this, please avoid jest.mock() on the file which exports ${funcGetName( + def, + )} or add jest.dontMock() on it.`, 'The same should be done for all related dependencies.', ].join(' '), ); diff --git a/libs/ng-mocks/src/lib/common/error.missing-decorators.ts b/libs/ng-mocks/src/lib/common/error.missing-decorators.ts index 6986a55067..4e9dbf116f 100644 --- a/libs/ng-mocks/src/lib/common/error.missing-decorators.ts +++ b/libs/ng-mocks/src/lib/common/error.missing-decorators.ts @@ -1,9 +1,11 @@ // tslint:disable strict-type-predicates +import funcGetName from './func.get-name'; + export default (def: any): void => { throw new Error( [ - `${def.name} declaration has been passed into ng-mocks without Angular decorators.`, + `${funcGetName(def)} declaration has been passed into ng-mocks without Angular decorators.`, 'Therefore, it cannot be properly handled.', 'Highly likely,', typeof jest === 'undefined' ? '' : /* istanbul ignore next */ 'jest.mock() has been used on its file, or', diff --git a/libs/ng-mocks/src/lib/common/func.get-mocked-ng-def-of.ts b/libs/ng-mocks/src/lib/common/func.get-mocked-ng-def-of.ts index 4a2f6614f9..9ea35ab429 100644 --- a/libs/ng-mocks/src/lib/common/func.get-mocked-ng-def-of.ts +++ b/libs/ng-mocks/src/lib/common/func.get-mocked-ng-def-of.ts @@ -6,12 +6,13 @@ import { MockedPipe } from '../mock-pipe/types'; import coreInjector from './core.injector'; import { NG_MOCKS } from './core.tokens'; import { AnyType, Type } from './core.types'; +import funcGetName from './func.get-name'; import { isMockedNgDefOf } from './func.is-mocked-ng-def-of'; import ngMocksUniverse from './ng-mocks-universe'; const getMock = (declaration: any, source: any, mocks?: Map) => { if (mocks && !mocks.has(source)) { - throw new Error(`There is no mock for ${source.name}`); + throw new Error(`There is no mock for ${funcGetName(source)}`); } let mock = mocks ? mocks.get(source) : undefined; if (mock === source) { @@ -75,5 +76,5 @@ export function getMockedNgDefOf(declaration: any, type?: any): any { return mock; } - throw new Error(`There is no mock for ${source.name}`); + throw new Error(`There is no mock for ${funcGetName(source)}`); } diff --git a/libs/ng-mocks/src/lib/common/func.get-name.spec.ts b/libs/ng-mocks/src/lib/common/func.get-name.spec.ts new file mode 100644 index 0000000000..5d299ff2b4 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.get-name.spec.ts @@ -0,0 +1,7 @@ +import funcGetName from './func.get-name'; + +describe('func.get-name', () => { + it('detects unknown', () => { + expect(funcGetName(false)).toEqual('unknown'); + }); +}); diff --git a/libs/ng-mocks/src/lib/common/func.get-name.ts b/libs/ng-mocks/src/lib/common/func.get-name.ts new file mode 100644 index 0000000000..1ffccff9ca --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.get-name.ts @@ -0,0 +1,16 @@ +export default (value: any): string => { + if (typeof value === 'function' && value.name) { + return value.name; + } + if (typeof value === 'function') { + return 'arrow-function'; + } + if (typeof value === 'object' && value && value.ngMetadataName === 'InjectionToken') { + return value._desc; + } + if (typeof value === 'object' && value && typeof value.constructor === 'function') { + return value.constructor.name; + } + + return 'unknown'; +}; diff --git a/libs/ng-mocks/src/lib/common/func.import-exists.ts b/libs/ng-mocks/src/lib/common/func.import-exists.ts index eeb37dc9ef..3774ac580d 100644 --- a/libs/ng-mocks/src/lib/common/func.import-exists.ts +++ b/libs/ng-mocks/src/lib/common/func.import-exists.ts @@ -1,5 +1,48 @@ +import funcGetName from './func.get-name'; +import { isNgDef } from './func.is-ng-def'; + export default (value: any, funcName: string) => { if (value === undefined || value === null) { throw new Error(`An empty parameter has been passed into ${funcName}. Please check that its import is correct.`); } + + if (funcName === 'MockPipe' && isNgDef(value, 'p')) { + return; + } + if (funcName === 'MockDirective' && isNgDef(value, 'd')) { + return; + } + if (funcName === 'MockComponent' && isNgDef(value, 'c')) { + return; + } + if (funcName === 'MockModule' && isNgDef(value, 'm')) { + return; + } + + const type = isNgDef(value, 'p') + ? 'pipe' + : isNgDef(value, 'd') + ? 'directive' + : isNgDef(value, 'c') + ? 'component' + : isNgDef(value, 'm') + ? 'module' + : isNgDef(value, 'i') + ? 'service' + : isNgDef(value, 't') + ? 'token' + : ''; + + if (type && funcName === 'MockPipe') { + throw new Error(`${funcName} accepts pipes, whereas ${funcGetName(value)} is a ${type}.`); + } + if (type && funcName === 'MockDirective') { + throw new Error(`${funcName} accepts directives, whereas ${funcGetName(value)} is a ${type}.`); + } + if (type && funcName === 'MockComponent') { + throw new Error(`${funcName} accepts components, whereas ${funcGetName(value)} is a ${type}.`); + } + if (type && funcName === 'MockModule') { + throw new Error(`${funcName} accepts modules, whereas ${funcGetName(value)} is a ${type}.`); + } }; diff --git a/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts b/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts index cb4102e188..6989f59423 100644 --- a/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts +++ b/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts @@ -4,6 +4,7 @@ import { IMockBuilderConfig } from '../mock-builder/types'; import coreConfig from './core.config'; import { AnyType } from './core.types'; +import funcGetName from './func.get-name'; // istanbul ignore next const getGlobal = (): any => window || global; @@ -70,7 +71,7 @@ const getDefaults = (def: any): [] | ['mock' | 'keep' | 'replace' | 'exclude', a } { - const defValue = typeof def === 'function' ? ngMocksUniverse.getDefaults().get(`@${def.name}`) : undefined; + const defValue = typeof def === 'function' ? ngMocksUniverse.getDefaults().get(`@${funcGetName(def)}`) : undefined; if (defValue) { return defValue; } diff --git a/libs/ng-mocks/src/lib/mock-declaration/mock-declaration.ts b/libs/ng-mocks/src/lib/mock-declaration/mock-declaration.ts index bfba84f78c..04ca144c30 100644 --- a/libs/ng-mocks/src/lib/mock-declaration/mock-declaration.ts +++ b/libs/ng-mocks/src/lib/mock-declaration/mock-declaration.ts @@ -2,6 +2,7 @@ import { Type } from '../common/core.types'; import errorJestMock from '../common/error.jest-mock'; +import funcGetName from '../common/func.get-name'; import { isNgDef } from '../common/func.is-ng-def'; import { MockComponent } from '../mock-component/mock-component'; import { MockedComponent } from '../mock-component/types'; @@ -33,7 +34,7 @@ export function MockDeclaration( throw new Error( [ 'MockDeclaration does not know how to mock', - typeof declaration === 'function' ? (declaration as any).name : declaration, + typeof declaration === 'function' ? funcGetName(declaration) : declaration, ].join(' '), ); } diff --git a/libs/ng-mocks/src/lib/mock-instance/mock-instance-forgot-reset.ts b/libs/ng-mocks/src/lib/mock-instance/mock-instance-forgot-reset.ts index 6ff5fd6410..d512ed3989 100644 --- a/libs/ng-mocks/src/lib/mock-instance/mock-instance-forgot-reset.ts +++ b/libs/ng-mocks/src/lib/mock-instance/mock-instance-forgot-reset.ts @@ -1,3 +1,4 @@ +import funcGetName from '../common/func.get-name'; import ngMocksUniverse from '../common/ng-mocks-universe'; export default (checkReset: Array<[any, any, any?]>) => { @@ -7,7 +8,7 @@ export default (checkReset: Array<[any, any, any?]>) => { while (checkReset.length) { const [declaration, config] = checkReset.pop() || /* istanbul ignore next */ []; if (config === ngMocksUniverse.configInstance.get(declaration)) { - showError.push(typeof declaration === 'function' ? declaration.name : declaration); + showError.push(typeof declaration === 'function' ? funcGetName(declaration) : declaration); } } diff --git a/libs/ng-mocks/src/lib/mock-module/mock-module.ts b/libs/ng-mocks/src/lib/mock-module/mock-module.ts index d02ba45294..3ce2bf9485 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-module.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-module.ts @@ -5,6 +5,7 @@ import { extendClass } from '../common/core.helpers'; import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; import { Type } from '../common/core.types'; import decorateMock from '../common/decorate.mock'; +import funcGetName from '../common/func.get-name'; import funcImportExists from '../common/func.import-exists'; import { isMockNgDef } from '../common/func.is-mock-ng-def'; import { isNgDef } from '../common/func.is-ng-def'; @@ -22,7 +23,7 @@ const flagReplace = (resolution?: string): boolean => resolution === 'replace' && !ngMocksUniverse.flags.has('skipMock'); const flagNever = (ngModule?: any): boolean => - coreConfig.neverMockModule.indexOf(ngModule.name) !== -1 && !ngMocksUniverse.flags.has('skipMock'); + coreConfig.neverMockModule.indexOf(funcGetName(ngModule)) !== -1 && !ngMocksUniverse.flags.has('skipMock'); const preprocessToggleFlag = (ngModule: Type): boolean => { let toggleSkipMockFlag = false; diff --git a/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts b/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts index f8663b91b8..f90867553d 100644 --- a/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts +++ b/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts @@ -4,6 +4,7 @@ import { extendClass } from '../common/core.helpers'; import coreReflectPipeResolve from '../common/core.reflect.pipe-resolve'; import { Type } from '../common/core.types'; import decorateMock from '../common/decorate.mock'; +import funcGetName from '../common/func.get-name'; import funcImportExists from '../common/func.import-exists'; import { isMockNgDef } from '../common/func.is-mock-ng-def'; import { Mock } from '../common/mock'; @@ -28,7 +29,7 @@ const getMockClass = (pipe: Type, transform?: PipeTransform['transform']): instance.transform = transform; } if (!instance.transform) { - helperMockService.mock(instance, 'transform', `${instance.constructor.name}.transform`); + helperMockService.mock(instance, 'transform', `${funcGetName(instance)}.transform`); } }, }); diff --git a/libs/ng-mocks/src/lib/mock-service/helper.create-mock-from-prototype.ts b/libs/ng-mocks/src/lib/mock-service/helper.create-mock-from-prototype.ts index cc7a34a475..dd2497e577 100644 --- a/libs/ng-mocks/src/lib/mock-service/helper.create-mock-from-prototype.ts +++ b/libs/ng-mocks/src/lib/mock-service/helper.create-mock-from-prototype.ts @@ -1,8 +1,10 @@ +import funcGetName from '../common/func.get-name'; + import helperMockService from './helper.mock-service'; import { MockedFunction } from './types'; export default (service: any): { [key in keyof any]: MockedFunction } => { - const mockName = service.constructor.name; + const mockName = funcGetName(service); const value: any = {}; const methods = helperMockService.extractMethodsFromPrototype(service); diff --git a/libs/ng-mocks/src/lib/mock-service/helper.extract-methods-from-prototype.ts b/libs/ng-mocks/src/lib/mock-service/helper.extract-methods-from-prototype.ts index 06c036056f..57de202f10 100644 --- a/libs/ng-mocks/src/lib/mock-service/helper.extract-methods-from-prototype.ts +++ b/libs/ng-mocks/src/lib/mock-service/helper.extract-methods-from-prototype.ts @@ -1,3 +1,5 @@ +import funcGetName from '../common/func.get-name'; + const sanitizerMethods = [ 'sanitize', 'bypassSecurityTrustHtml', @@ -14,7 +16,7 @@ const extraMethods: Record = { const getOwnPropertyNames = (prototype: any): string[] => { const result: string[] = Object.getOwnPropertyNames(prototype); - for (const method of extraMethods[prototype.constructor.name] ?? []) { + for (const method of extraMethods[funcGetName(prototype)] ?? []) { result.push(method); } diff --git a/libs/ng-mocks/src/lib/mock-service/helper.mock.ts b/libs/ng-mocks/src/lib/mock-service/helper.mock.ts index e3df1c7b6d..404eb3016c 100644 --- a/libs/ng-mocks/src/lib/mock-service/helper.mock.ts +++ b/libs/ng-mocks/src/lib/mock-service/helper.mock.ts @@ -1,16 +1,12 @@ +import funcGetName from '../common/func.get-name'; + import helperMockService from './helper.mock-service'; import { MockedFunction } from './types'; // istanbul ignore next const createName = (name: string, mockName?: string, instance?: any, accessType?: string) => `${ - mockName - ? mockName - : typeof instance.prototype === 'function' - ? instance.prototype.name - : typeof instance.constructor === 'function' - ? instance.constructor.name - : 'unknown' + mockName ? mockName : typeof instance.prototype === 'function' ? instance.prototype.name : funcGetName(instance) }.${name}${accessType || ''}`; const generateMockDef = (def: any, mock: any, accessType?: string): PropertyDescriptor => ({ diff --git a/libs/ng-mocks/src/lib/mock-service/mock-service.ts b/libs/ng-mocks/src/lib/mock-service/mock-service.ts index 53ed9f3d3a..a3df3322b5 100644 --- a/libs/ng-mocks/src/lib/mock-service/mock-service.ts +++ b/libs/ng-mocks/src/lib/mock-service/mock-service.ts @@ -1,4 +1,5 @@ import { AnyType } from '../common/core.types'; +import funcGetName from '../common/func.get-name'; import mockHelperStub from '../mock-helper/mock-helper.stub'; import checkIsClass from './check.is-class'; @@ -12,8 +13,7 @@ const mockVariableMap: Array< [checkIsClass, (service: any) => helperMockService.createMockFromPrototype(service.prototype)], [ checkIsFunc, - (service: any, prefix: string) => - helperMockService.mockFunction(`func:${prefix || service.name || 'arrow-function'}`), + (service: any, prefix: string) => helperMockService.mockFunction(`func:${prefix || funcGetName(service)}`), ], [def => Array.isArray(def), () => []], [ diff --git a/tests/issue-1168/test.spec.ts b/tests/issue-1168/test.spec.ts new file mode 100644 index 0000000000..4710f0f233 --- /dev/null +++ b/tests/issue-1168/test.spec.ts @@ -0,0 +1,234 @@ +import { + Component, + Directive, + Injectable, + InjectionToken, + NgModule, + Pipe, + PipeTransform, +} from '@angular/core'; +import { + MockComponent, + MockDirective, + MockModule, + MockPipe, +} from 'ng-mocks'; + +class TargetClass {} + +@Pipe({ + name: 'target', +}) +class TargetPipe implements PipeTransform { + public transform(value: number): string { + return `${value}`; + } +} + +@Directive({ + selector: 'target', +}) +class TargetDirective {} + +@Component({ + selector: 'target', + template: 'target', +}) +class TargetComponent {} + +@Injectable() +class TargetService {} + +const TOKEN = new InjectionToken('TOKEN'); + +@NgModule({ + declarations: [TargetPipe, TargetDirective, TargetComponent], + providers: [ + TargetService, + { + provide: TOKEN, + useValue: 'TOKEN', + }, + ], +}) +class TargetModule {} + +// We should try to detect a wrong declaration. +// @see https://github.com/ike18t/ng-mocks/issues/354#issuecomment-927694500 +describe('issue-1168', () => { + describe('MockPipe', () => { + it('fails on TargetClass', () => { + expect(() => MockPipe(TargetClass as any)).toThrowError( + /ng-mocks is not in JIT mode/, + ); + }); + + it('passes on TargetPipe', () => { + expect(() => MockPipe(TargetPipe as any)).not.toThrow(); + }); + + it('fails on TargetDirective', () => { + expect(() => MockPipe(TargetDirective as any)).toThrowError( + 'MockPipe accepts pipes, whereas TargetDirective is a directive.', + ); + }); + + it('fails on TargetComponent', () => { + expect(() => MockPipe(TargetComponent as any)).toThrowError( + 'MockPipe accepts pipes, whereas TargetComponent is a component.', + ); + }); + + it('fails on TargetService', () => { + expect(() => MockPipe(TargetService as any)).toThrowError( + 'MockPipe accepts pipes, whereas TargetService is a service.', + ); + }); + + it('fails on TOKEN', () => { + expect(() => MockPipe(TOKEN as any)).toThrowError( + 'MockPipe accepts pipes, whereas TOKEN is a token.', + ); + }); + + it('fails on TargetModule', () => { + expect(() => MockPipe(TargetModule as any)).toThrowError( + 'MockPipe accepts pipes, whereas TargetModule is a module.', + ); + }); + }); + + describe('MockDirective', () => { + it('fails on TargetClass', () => { + expect(() => MockDirective(TargetClass as any)).toThrowError( + /ng-mocks is not in JIT mode/, + ); + }); + + it('fails on TargetPipe', () => { + expect(() => MockDirective(TargetPipe as any)).toThrowError( + 'MockDirective accepts directives, whereas TargetPipe is a pipe.', + ); + }); + + it('passes on TargetDirective', () => { + expect(() => + MockDirective(TargetDirective as any), + ).not.toThrow(); + }); + + it('fails on TargetComponent', () => { + expect(() => + MockDirective(TargetComponent as any), + ).toThrowError( + 'MockDirective accepts directives, whereas TargetComponent is a component.', + ); + }); + + it('fails on TargetService', () => { + expect(() => MockDirective(TargetService as any)).toThrowError( + 'MockDirective accepts directives, whereas TargetService is a service.', + ); + }); + + it('fails on TOKEN', () => { + expect(() => MockDirective(TOKEN as any)).toThrowError( + 'MockDirective accepts directives, whereas TOKEN is a token.', + ); + }); + + it('fails on TargetModule', () => { + expect(() => MockDirective(TargetModule as any)).toThrowError( + 'MockDirective accepts directives, whereas TargetModule is a module.', + ); + }); + }); + + describe('MockComponent', () => { + it('fails on TargetClass', () => { + expect(() => MockComponent(TargetClass as any)).toThrowError( + /ng-mocks is not in JIT mode/, + ); + }); + + it('fails on TargetPipe', () => { + expect(() => MockComponent(TargetPipe as any)).toThrowError( + 'MockComponent accepts components, whereas TargetPipe is a pipe.', + ); + }); + + it('fails on TargetDirective', () => { + expect(() => + MockComponent(TargetDirective as any), + ).toThrowError( + 'MockComponent accepts components, whereas TargetDirective is a directive.', + ); + }); + + it('passes on TargetComponent', () => { + expect(() => + MockComponent(TargetComponent as any), + ).not.toThrow(); + }); + + it('fails on TargetService', () => { + expect(() => MockComponent(TargetService as any)).toThrowError( + 'MockComponent accepts components, whereas TargetService is a service.', + ); + }); + + it('fails on TOKEN', () => { + expect(() => MockComponent(TOKEN as any)).toThrowError( + 'MockComponent accepts components, whereas TOKEN is a token.', + ); + }); + + it('fails on TargetModule', () => { + expect(() => MockComponent(TargetModule as any)).toThrowError( + 'MockComponent accepts components, whereas TargetModule is a module.', + ); + }); + }); + + describe('MockModule', () => { + it('fails on TargetClass', () => { + expect(() => MockModule(TargetClass as any)).toThrowError( + /ng-mocks is not in JIT mode/, + ); + }); + + it('fails on TargetPipe', () => { + expect(() => MockModule(TargetPipe as any)).toThrowError( + 'MockModule accepts modules, whereas TargetPipe is a pipe.', + ); + }); + + it('fails on TargetDirective', () => { + expect(() => MockModule(TargetDirective as any)).toThrowError( + 'MockModule accepts modules, whereas TargetDirective is a directive.', + ); + }); + + it('fails on TargetComponent', () => { + expect(() => MockModule(TargetComponent as any)).toThrowError( + 'MockModule accepts modules, whereas TargetComponent is a component.', + ); + }); + + it('fails on TargetService', () => { + expect(() => MockModule(TargetService as any)).toThrowError( + 'MockModule accepts modules, whereas TargetService is a service.', + ); + }); + + it('fails on TOKEN', () => { + expect(() => MockModule(TOKEN as any)).toThrowError( + 'MockModule accepts modules, whereas TOKEN is a token.', + ); + }); + + it('passes on TargetModule', () => { + expect(() => MockModule(TargetModule as any)).not.toThrow(); + }); + }); +});