From d19f95809a9e5802f201ddce31372deecf95393d Mon Sep 17 00:00:00 2001 From: MG Date: Thu, 22 Oct 2020 23:28:34 +0200 Subject: [PATCH] feat: ngMocks.guts for easy start --- examples/ngMocksGuts/test.spec.ts | 183 +++++++ lib/common/lib.ts | 88 +-- lib/mock-builder/mock-builder.ts | 4 +- lib/mock-declaration/mock-declaration.ts | 8 +- lib/mock-helper/mock-helper.faster.ts | 30 + lib/mock-helper/mock-helper.find.ts | 24 + lib/mock-helper/mock-helper.findAll.ts | 11 + lib/mock-helper/mock-helper.findInstance.ts | 23 + lib/mock-helper/mock-helper.findInstances.ts | 31 ++ lib/mock-helper/mock-helper.flushTestBed.ts | 10 + lib/mock-helper/mock-helper.get.ts | 41 ++ lib/mock-helper/mock-helper.guts.spec.ts | 517 ++++++++++++++++++ lib/mock-helper/mock-helper.guts.ts | 158 ++++++ lib/mock-helper/mock-helper.input.ts | 47 ++ lib/mock-helper/mock-helper.output.ts | 47 ++ lib/mock-helper/mock-helper.reset.ts | 13 + lib/mock-helper/mock-helper.stub.ts | 17 + lib/mock-helper/mock-helper.ts | 276 ++-------- lib/mock-module/mock-module.spec.ts | 57 +- lib/mock-module/mock-module.ts | 26 +- .../fixtures.modules.ts | 6 +- .../test.spec.ts | 2 +- 22 files changed, 1326 insertions(+), 293 deletions(-) create mode 100644 examples/ngMocksGuts/test.spec.ts create mode 100644 lib/mock-helper/mock-helper.faster.ts create mode 100644 lib/mock-helper/mock-helper.find.ts create mode 100644 lib/mock-helper/mock-helper.findAll.ts create mode 100644 lib/mock-helper/mock-helper.findInstance.ts create mode 100644 lib/mock-helper/mock-helper.findInstances.ts create mode 100644 lib/mock-helper/mock-helper.flushTestBed.ts create mode 100644 lib/mock-helper/mock-helper.get.ts create mode 100644 lib/mock-helper/mock-helper.guts.spec.ts create mode 100644 lib/mock-helper/mock-helper.guts.ts create mode 100644 lib/mock-helper/mock-helper.input.ts create mode 100644 lib/mock-helper/mock-helper.output.ts create mode 100644 lib/mock-helper/mock-helper.reset.ts create mode 100644 lib/mock-helper/mock-helper.stub.ts diff --git a/examples/ngMocksGuts/test.spec.ts b/examples/ngMocksGuts/test.spec.ts new file mode 100644 index 0000000000..c12af9b2f8 --- /dev/null +++ b/examples/ngMocksGuts/test.spec.ts @@ -0,0 +1,183 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + Directive, + EventEmitter, + Inject, + Injectable, + InjectionToken, + Input, + NgModule, + NO_ERRORS_SCHEMA, + OnDestroy, + Output, + Pipe, + PipeTransform, +} from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockDirective, MockModule, MockPipe, MockService, ngMocks } from 'ng-mocks'; + +const TARGET1 = new InjectionToken('TARGET1'); + +@Injectable() +class Target1Service { + public callback: () => void = () => undefined; + + public touch(): void { + this.callback(); + } +} + +@Pipe({ + name: 'target1', +}) +class Target1Pipe implements PipeTransform { + protected readonly name = 'pipe1'; + public transform(value: string): string { + return `${this.name}:${value}`; + } +} + +@Component({ + selector: 'target2', + template: ``, +}) +class Target2Component {} + +@Component({ + selector: 'target1', + template: `
+ {{ greeting }} {{ greeting | target1 }} {{ target }} +
`, +}) +class Target1Component { + @Input() public greeting: string | null = null; + public readonly target: string; + @Output() public readonly update: EventEmitter = new EventEmitter(); + + constructor(@Inject(TARGET1) target: string) { + this.target = target; + } +} + +@Directive({ + selector: '[target1]', +}) +class Target1Directive implements OnDestroy { + public readonly service: Target1Service; + @Output() public readonly target1: EventEmitter = new EventEmitter(); + + constructor(service: Target1Service) { + this.service = service; + this.service.callback = () => this.target1.emit(); + } + + ngOnDestroy(): void { + this.service.callback = () => undefined; + } +} + +@NgModule({ + declarations: [Target2Component], + exports: [Target2Component], + providers: [ + { + provide: TARGET1, + useValue: 'target1', + }, + ], +}) +class Target2Module {} + +@NgModule({ + declarations: [Target1Pipe, Target1Component, Target1Directive], + imports: [CommonModule, Target2Module], + providers: [ + Target1Service, + { + provide: TARGET1, + useValue: 'target1', + }, + ], +}) +class Target1Module {} + +describe('ngMocks.guts:NO_ERRORS_SCHEMA', () => { + let fixture: ComponentFixture; + let component: Target1Component; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [Target1Component, Target1Pipe], + imports: [CommonModule], + providers: [ + { + provide: TARGET1, + useValue: 'target1', + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }); + fixture = TestBed.createComponent(Target1Component); + component = fixture.componentInstance; + }); + + it('creates component', () => { + expect(component).toEqual(jasmine.any(Target1Component)); + expect(fixture.nativeElement.innerHTML).toEqual('
'); + component.greeting = 'hello'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('hello'); + }); +}); + +describe('ngMocks.guts:legacy', () => { + let fixture: ComponentFixture; + let component: Target1Component; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MockPipe(Target1Pipe), MockDirective(Target1Directive), Target1Component], + imports: [CommonModule, MockModule(Target2Module)], + providers: [ + { + provide: Target1Service, + useValue: MockService(Target1Service), + }, + { + provide: TARGET1, + useValue: undefined, + }, + ], + }); + fixture = TestBed.createComponent(Target1Component); + component = fixture.componentInstance; + }); + + it('creates component', () => { + expect(component).toEqual(jasmine.any(Target1Component)); + expect(fixture.nativeElement.innerHTML).toEqual('
'); + component.greeting = 'hello'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('hello'); + }); +}); + +describe('ngMocks.guts:normal', () => { + let fixture: ComponentFixture; + let component: Target1Component; + + beforeEach(() => { + TestBed.configureTestingModule(ngMocks.guts(Target1Component, Target1Module)); + fixture = TestBed.createComponent(Target1Component); + component = fixture.componentInstance; + }); + + it('creates component', () => { + expect(component).toEqual(jasmine.any(Target1Component)); + expect(fixture.nativeElement.innerHTML).toEqual('
'); + component.greeting = 'hello'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('hello'); + }); +}); diff --git a/lib/common/lib.ts b/lib/common/lib.ts index ea493312f9..d5d3ae059a 100644 --- a/lib/common/lib.ts +++ b/lib/common/lib.ts @@ -109,8 +109,8 @@ export const extendClass = (base: Type): Type => { return child; }; -export const isNgType = (object: Type, type: string): boolean => - jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type); +export const isNgType = (declaration: Type, type: string): boolean => + jitReflector.annotations(declaration).some(annotation => annotation.ngMetadataName === type); /** * Checks whether a class was decorated by a ng type. @@ -119,14 +119,14 @@ export const isNgType = (object: Type, type: string): boolean => * d - directive. * p - pipe. */ -export function isNgDef(object: any, ngType: 'm' | 'c' | 'd'): object is Type; -export function isNgDef(object: any, ngType: 'p'): object is Type; -export function isNgDef(object: any): object is Type; -export function isNgDef(object: any, ngType?: string): object is Type { - const isModule = (!ngType || ngType === 'm') && isNgType(object, 'NgModule'); - const isComponent = (!ngType || ngType === 'c') && isNgType(object, 'Component'); - const isDirective = (!ngType || ngType === 'd') && isNgType(object, 'Directive'); - const isPipe = (!ngType || ngType === 'p') && isNgType(object, 'Pipe'); +export function isNgDef(declaration: any, ngType: 'm' | 'c' | 'd'): declaration is Type; +export function isNgDef(declaration: any, ngType: 'p'): declaration is Type; +export function isNgDef(declaration: any): declaration is Type; +export function isNgDef(declaration: any, ngType?: string): declaration is Type { + const isModule = (!ngType || ngType === 'm') && isNgType(declaration, 'NgModule'); + const isComponent = (!ngType || ngType === 'c') && isNgType(declaration, 'Component'); + const isDirective = (!ngType || ngType === 'd') && isNgType(declaration, 'Directive'); + const isPipe = (!ngType || ngType === 'p') && isNgType(declaration, 'Pipe'); return isModule || isComponent || isDirective || isPipe; } @@ -160,12 +160,12 @@ export function isMockedNgDefOf(declaration: any, type: Type, ngType?: any ); } -export const isNgInjectionToken = (object: any): object is InjectionToken => - typeof object === 'object' && object.ngMetadataName === 'InjectionToken'; +export const isNgInjectionToken = (token: any): token is InjectionToken => + typeof token === 'object' && token.ngMetadataName === 'InjectionToken'; // Checks if an object implements ModuleWithProviders. -export const isNgModuleDefWithProviders = (object: any): object is NgModuleWithProviders => - object.ngModule !== undefined && isNgDef(object.ngModule, 'm'); +export const isNgModuleDefWithProviders = (declaration: any): declaration is NgModuleWithProviders => + declaration.ngModule !== undefined && isNgDef(declaration.ngModule, 'm'); /** * Checks whether an object is an instance of a mocked class that was decorated by a ng type. @@ -174,17 +174,21 @@ export const isNgModuleDefWithProviders = (object: any): object is NgModuleWithP * d - directive. * p - pipe. */ -export function isMockOf(object: any, type: Type, ngType: 'm'): object is MockedModule; -export function isMockOf(object: any, type: Type, ngType: 'c'): object is MockedComponent; -export function isMockOf(object: any, type: Type, ngType: 'd'): object is MockedDirective; -export function isMockOf(object: any, type: Type, ngType: 'p'): object is MockedPipe; -export function isMockOf(object: any, type: Type): object is T; -export function isMockOf(object: any, type: Type, ngType?: any): object is T { +export function isMockOf(instance: any, declaration: Type, ngType: 'm'): instance is MockedModule; +export function isMockOf(instance: any, declaration: Type, ngType: 'c'): instance is MockedComponent; +export function isMockOf(instance: any, declaration: Type, ngType: 'd'): instance is MockedDirective; +export function isMockOf( + instance: any, + declaration: Type, + ngType: 'p' +): instance is MockedPipe; +export function isMockOf(instance: any, declaration: Type): instance is T; +export function isMockOf(instance: any, declaration: Type, ngType?: any): instance is T { return ( - typeof object === 'object' && - object.__ngMocksMock && - object.constructor === type && - (ngType ? isNgDef(object.constructor, ngType) : isNgDef(object.constructor)) + typeof instance === 'object' && + instance.__ngMocksMock && + instance.constructor === declaration && + (ngType ? isNgDef(instance.constructor, ngType) : isNgDef(instance.constructor)) ); } @@ -195,13 +199,13 @@ export function isMockOf(object: any, type: Type, ngType?: any): object is * d - directive. * p - pipe. */ -export function getMockedNgDefOf(type: Type, ngType: 'm'): Type>; -export function getMockedNgDefOf(type: Type, ngType: 'c'): Type>; -export function getMockedNgDefOf(type: Type, ngType: 'd'): Type>; -export function getMockedNgDefOf(type: Type, ngType: 'p'): Type>; -export function getMockedNgDefOf(type: Type): Type; -export function getMockedNgDefOf(type: any, ngType?: any): any { - const source = type.mockOf ? type.mockOf : type; +export function getMockedNgDefOf(declaration: Type, type: 'm'): Type>; +export function getMockedNgDefOf(declaration: Type, type: 'c'): Type>; +export function getMockedNgDefOf(declaration: Type, type: 'd'): Type>; +export function getMockedNgDefOf(declaration: Type, type: 'p'): Type>; +export function getMockedNgDefOf(declaration: Type): Type; +export function getMockedNgDefOf(declaration: any, type?: any): any { + const source = declaration.mockOf ? declaration.mockOf : declaration; const mocks = getTestBedInjection(NG_MOCKS); let mock: any; @@ -214,16 +218,16 @@ export function getMockedNgDefOf(type: any, ngType?: any): any { } // If we are not in the MockBuilder env we can rely on the current cache. - if (!mock && source !== type) { - mock = type; + if (!mock && source !== declaration) { + mock = declaration; } else if (!mock && ngMocksUniverse.cacheMocks.has(source)) { mock = ngMocksUniverse.cacheMocks.get(source); } - if (mock && !ngType) { + if (mock && !type) { return mock; } - if (mock && ngType && isMockedNgDefOf(mock, source, ngType)) { + if (mock && type && isMockedNgDefOf(mock, source, type)) { return mock; } @@ -231,11 +235,11 @@ export function getMockedNgDefOf(type: any, ngType?: any): any { throw new Error(`There is no mock for ${source.name}`); } -export function getSourceOfMock(type: Type>): Type; -export function getSourceOfMock(type: Type>): Type; -export function getSourceOfMock(type: Type>): Type; -export function getSourceOfMock(type: Type>): Type; -export function getSourceOfMock(type: Type): Type; -export function getSourceOfMock(type: any): Type { - return typeof type === 'function' && type.mockOf ? type.mockOf : type; +export function getSourceOfMock(declaration: Type>): Type; +export function getSourceOfMock(declaration: Type>): Type; +export function getSourceOfMock(declaration: Type>): Type; +export function getSourceOfMock(declaration: Type>): Type; +export function getSourceOfMock(declaration: Type): Type; +export function getSourceOfMock(declaration: any): Type { + return typeof declaration === 'function' && declaration.mockOf ? declaration.mockOf : declaration; } diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts index e92ecd9cba..366545893b 100644 --- a/lib/mock-builder/mock-builder.ts +++ b/lib/mock-builder/mock-builder.ts @@ -1,7 +1,7 @@ import { MetadataOverride, TestBed, TestModuleMetadata } from '@angular/core/testing'; -import { ngMocksUniverse } from 'ng-mocks/dist/lib/common/ng-mocks-universe'; -import { AnyType, flatten, isNgDef, mapEntries, NG_MOCKS, NG_MOCKS_OVERRIDES, Type } from '../common'; +import { AnyType, flatten, isNgDef, mapEntries, NG_MOCKS, NG_MOCKS_OVERRIDES, Type } from '../common/lib'; +import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { ngMocks } from '../mock-helper/mock-helper'; import { MockBuilderPerformance } from './mock-builder-performance'; diff --git a/lib/mock-declaration/mock-declaration.ts b/lib/mock-declaration/mock-declaration.ts index f47a26487e..76ceba6e9a 100644 --- a/lib/mock-declaration/mock-declaration.ts +++ b/lib/mock-declaration/mock-declaration.ts @@ -1,4 +1,4 @@ -import { AbstractType, isNgDef, Type } from '../common'; +import { AnyType, isNgDef, Type } from '../common'; import { MockComponent, MockedComponent } from '../mock-component'; import { MockDirective, MockedDirective } from '../mock-directive'; import { MockedPipe, MockPipe } from '../mock-pipe'; @@ -7,9 +7,11 @@ export function MockDeclarations(...declarations: Array>): Array(declaration: Type): Type | MockedDirective | MockedComponent>; export function MockDeclaration( - declaration: AbstractType + declaration: AnyType +): Type | MockedDirective | MockedComponent>; +export function MockDeclaration( + declaration: AnyType ): Type | MockedDirective | MockedComponent>; export function MockDeclaration( declaration: Type diff --git a/lib/mock-helper/mock-helper.faster.ts b/lib/mock-helper/mock-helper.faster.ts new file mode 100644 index 0000000000..a9a6e2dbed --- /dev/null +++ b/lib/mock-helper/mock-helper.faster.ts @@ -0,0 +1,30 @@ +// tslint:disable:no-default-export no-default-import + +import { getTestBed, TestBed } from '@angular/core/testing'; + +import { ngMocksUniverse } from '../common/ng-mocks-universe'; + +import flushTestBed from './mock-helper.flushTestBed'; + +export default () => { + beforeAll(() => { + if (ngMocksUniverse.global.has('bullet:customized')) { + TestBed.resetTestingModule(); + } + ngMocksUniverse.global.set('bullet', true); + }); + + afterEach(() => { + flushTestBed(); + for (const fixture of (getTestBed() as any)._activeFixtures || /* istanbul ignore next */ []) { + fixture.destroy(); + } + }); + + afterAll(() => { + ngMocksUniverse.global.delete('bullet'); + if (ngMocksUniverse.global.has('bullet:reset')) { + TestBed.resetTestingModule(); + } + }); +}; diff --git a/lib/mock-helper/mock-helper.find.ts b/lib/mock-helper/mock-helper.find.ts new file mode 100644 index 0000000000..b440178a58 --- /dev/null +++ b/lib/mock-helper/mock-helper.find.ts @@ -0,0 +1,24 @@ +// tslint:disable:no-default-export + +import { By } from '@angular/platform-browser'; + +import { getSourceOfMock, Type } from '../common'; +import { MockedDebugElement } from '../mock-render'; + +const defaultNotFoundValue = {}; // simulating Symbol + +export default (...args: any[]) => { + const el: MockedDebugElement = args[0]; + const sel: string | Type = args[1]; + const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; + + const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); + const result = el.query(term); + if (result) { + return result; + } + if (notFoundValue !== defaultNotFoundValue) { + return notFoundValue; + } + throw new Error(`Cannot find an element via ngMocks.find(${typeof sel === 'string' ? sel : sel.name})`); +}; diff --git a/lib/mock-helper/mock-helper.findAll.ts b/lib/mock-helper/mock-helper.findAll.ts new file mode 100644 index 0000000000..53b4ce6145 --- /dev/null +++ b/lib/mock-helper/mock-helper.findAll.ts @@ -0,0 +1,11 @@ +// tslint:disable:no-default-export + +import { By } from '@angular/platform-browser'; + +import { getSourceOfMock } from '../common'; +import { MockedDebugElement } from '../mock-render'; + +export default (el: MockedDebugElement, sel: any) => { + const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); + return el.queryAll(term); +}; diff --git a/lib/mock-helper/mock-helper.findInstance.ts b/lib/mock-helper/mock-helper.findInstance.ts new file mode 100644 index 0000000000..9bcb03dcca --- /dev/null +++ b/lib/mock-helper/mock-helper.findInstance.ts @@ -0,0 +1,23 @@ +// tslint:disable:no-default-export no-default-import + +import { getSourceOfMock, Type } from '../common'; +import { MockedDebugElement } from '../mock-render'; + +import findInstances from './mock-helper.findInstances'; + +const defaultNotFoundValue = {}; // simulating Symbol + +export default (...args: any[]) => { + const el: MockedDebugElement = args[0]; + const sel: Type = args[1]; + const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; + + const result = findInstances(el, getSourceOfMock(sel)); + if (result.length) { + return result[0]; + } + if (notFoundValue !== defaultNotFoundValue) { + return notFoundValue; + } + throw new Error(`Cannot find ${sel.name} directive via ngMocks.findInstance`); +}; diff --git a/lib/mock-helper/mock-helper.findInstances.ts b/lib/mock-helper/mock-helper.findInstances.ts new file mode 100644 index 0000000000..b906d5ebb7 --- /dev/null +++ b/lib/mock-helper/mock-helper.findInstances.ts @@ -0,0 +1,31 @@ +// tslint:disable:no-default-export + +import { getSourceOfMock, Type } from '../common'; +import { MockedDebugNode } from '../mock-render'; + +function nestedCheck( + result: T[], + node: MockedDebugNode & { childNodes?: MockedDebugNode[] }, + callback: (node: MockedDebugNode) => undefined | T +) { + const element = callback(node); + if (element) { + result.push(element); + } + const childNodes = node.childNodes ? node.childNodes : []; + childNodes.forEach(childNode => { + nestedCheck(result, childNode, callback); + }); +} + +export default (el: MockedDebugNode, sel: Type): T[] => { + const result: T[] = []; + nestedCheck(result, el, node => { + try { + return node.injector.get(getSourceOfMock(sel)); + } catch (error) { + return undefined; + } + }); + return result; +}; diff --git a/lib/mock-helper/mock-helper.flushTestBed.ts b/lib/mock-helper/mock-helper.flushTestBed.ts new file mode 100644 index 0000000000..724d044ebb --- /dev/null +++ b/lib/mock-helper/mock-helper.flushTestBed.ts @@ -0,0 +1,10 @@ +// tslint:disable:no-default-export + +import { getTestBed } from '@angular/core/testing'; + +export default (): void => { + const testBed: any = getTestBed(); + testBed._instantiated = false; + testBed._moduleFactory = undefined; + testBed._testModuleRef = null; +}; diff --git a/lib/mock-helper/mock-helper.get.ts b/lib/mock-helper/mock-helper.get.ts new file mode 100644 index 0000000000..fec8493049 --- /dev/null +++ b/lib/mock-helper/mock-helper.get.ts @@ -0,0 +1,41 @@ +// tslint:disable:no-default-export + +import { getSourceOfMock, Type } from '../common'; +import { MockedDebugElement } from '../mock-render'; + +const defaultNotFoundValue = {}; // simulating Symbol + +export default (...args: any[]) => { + const el: MockedDebugElement = args[0]; + const sel: Type = args[1]; + const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; + let notFound = false; + + // Looking for related attribute directive. + try { + return el.injector.get(getSourceOfMock(sel)); + } catch (error) { + // looks like the directive is structural. + } + + // Looking for related structural directive. + // It's located as prev node. + const prevNode = el.nativeNode.previousSibling; + if (!prevNode || prevNode.nodeName !== '#comment') { + notFound = true; + } + const matches = notFound || !el || !el.parent ? [] : el.parent.queryAllNodes(node => node.nativeNode === prevNode); + if (matches.length === 0) { + notFound = true; + } + const matchedNode = matches[0]; + try { + return matchedNode.injector.get(getSourceOfMock(sel)); + } catch (error) { + notFound = true; + } + if (notFound && notFoundValue !== defaultNotFoundValue) { + return notFoundValue; + } + throw new Error(`Cannot find ${sel.name} directive via ngMocks.get`); +}; diff --git a/lib/mock-helper/mock-helper.guts.spec.ts b/lib/mock-helper/mock-helper.guts.spec.ts new file mode 100644 index 0000000000..94c401d394 --- /dev/null +++ b/lib/mock-helper/mock-helper.guts.spec.ts @@ -0,0 +1,517 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + Directive, + EventEmitter, + Inject, + Injectable, + InjectionToken, + Injector, + Input, + NgModule, + OnDestroy, + Output, + Pipe, + PipeTransform, +} from '@angular/core'; +import { isMockedNgDefOf, isNgDef, ngMocks } from 'ng-mocks'; + +const TARGET1 = new InjectionToken('TARGET1'); +const TARGET2 = new InjectionToken('TARGET2'); + +@Injectable() +class Target1Service { + public callback: () => void = () => undefined; + + public touch(): void { + this.callback(); + } +} + +@Pipe({ + name: 'target1', +}) +class Target1Pipe implements PipeTransform { + protected readonly name = 'pipe1'; + public transform(value: string): string { + return `${this.name}:${value}`; + } +} + +@Component({ + selector: 'target2', + template: ``, +}) +class Target2Component {} + +@Component({ + selector: 'target1', + template: `
{{ greeting | target1 }} {{ target }}
`, +}) +class Target1Component { + @Input() public readonly greeting: string | null = null; + public readonly target: string; + @Output() public readonly update: EventEmitter = new EventEmitter(); + + constructor(@Inject(TARGET1) target: string) { + this.target = target; + } +} + +@Directive({ + selector: '[target1]', +}) +class Target1Directive implements OnDestroy { + public readonly service: Target1Service; + @Output() public readonly target1: EventEmitter = new EventEmitter(); + + constructor(service: Target1Service) { + this.service = service; + this.service.callback = () => this.target1.emit(); + } + + ngOnDestroy(): void { + this.service.callback = () => undefined; + } +} + +@NgModule({ + declarations: [Target2Component], + exports: [Target2Component], + providers: [ + { + provide: TARGET1, + useValue: 'target1', + }, + ], +}) +class Target2Module {} + +@NgModule({ + declarations: [Target1Pipe, Target1Component, Target1Directive], + imports: [CommonModule, Target2Module], + providers: [ + Target1Service, + { + provide: TARGET1, + useValue: 'target1', + }, + ], +}) +class Target1Module {} + +@NgModule({ + exports: [Target2Module], + imports: [CommonModule, Target2Module], +}) +class Target3Module {} + +describe('mock-helper.guts', () => { + it('mocks guts, but keeps the declaration', () => { + const ngModule = ngMocks.guts(Target1Component, Target1Module); + expect(ngModule).toBeDefined(); + expect(ngModule.declarations?.length).toEqual(3); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'p')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Pipe, 'p')).toBeTruthy(); + expect(isNgDef(ngModule.declarations[1], 'c')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[1], Target1Component, 'c')).toBeFalsy(); + expect(isNgDef(ngModule.declarations[2], 'd')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[2], Target1Directive, 'd')).toBeTruthy(); + } + expect(ngModule.imports?.length).toEqual(2); + if (ngModule.imports) { + expect(isNgDef(ngModule.imports[0], 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[0], CommonModule, 'm')).toBeFalsy(); + expect(isNgDef(ngModule.imports[1], 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[1], Target2Module, 'm')).toBeTruthy(); + } + expect(ngModule.providers?.length).toEqual(2); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + deps: [Injector], + provide: Target1Service, + useFactory: jasmine.anything(), + }); + expect(ngModule.providers[1]).toEqual({ + provide: TARGET1, + useValue: '', + }); + } + }); + + it('keeps module', () => { + const ngModule = ngMocks.guts(Target1Module); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(isNgDef(ngModule.imports[0], 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[0], Target1Module, 'm')).toBeFalsy(); + } + }); + + it('keeps module with providers', () => { + const ngModule = ngMocks.guts(Target1Module, { + ngModule: Target1Module, + providers: [], + }); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(isNgDef(ngModule.imports[0].ngModule, 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[0].ngModule, Target1Module, 'm')).toBeFalsy(); + } + }); + + it('keeps component', () => { + const ngModule = ngMocks.guts(Target1Component); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'c')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Component, 'c')).toBeFalsy(); + } + }); + + it('keeps directive', () => { + const ngModule = ngMocks.guts(Target1Directive); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'd')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Directive, 'd')).toBeFalsy(); + } + }); + + it('keeps pipe', () => { + const ngModule = ngMocks.guts(Target1Pipe); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'p')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Pipe, 'p')).toBeFalsy(); + } + }); + + it('keeps service', () => { + const ngModule = ngMocks.guts(Target1Service); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual(Target1Service); + } + }); + + it('keeps customized service', () => { + const ngModule = ngMocks.guts(Target1Service, { + provide: Target1Service, + useValue: 123, + }); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + provide: Target1Service, + useValue: 123, + }); + } + }); + + it('keeps tokens', () => { + const ngModule = ngMocks.guts(TARGET1, { provide: TARGET1, useValue: 123 }); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + provide: TARGET1, + useValue: 123, + }); + } + }); + + it('skips kept tokens', () => { + const ngModule = ngMocks.guts(TARGET1); + expect(ngModule.providers?.length).toEqual(0); + }); + + it('mocks module', () => { + const ngModule = ngMocks.guts(TARGET2, Target1Module); + expect(ngModule.imports?.length).toEqual(2); + if (ngModule.imports) { + expect(ngModule.imports[0]).toBe(CommonModule); + expect(isMockedNgDefOf(ngModule.imports[1], Target2Module, 'm')).toBeTruthy(); + } + }); + + it('mocks module with providers', () => { + const ngModule = ngMocks.guts(TARGET2, { + ngModule: Target1Module, + providers: [], + }); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(isNgDef(ngModule.imports[0].ngModule, 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[0].ngModule, Target1Module, 'm')).toBeTruthy(); + } + }); + + it('mocks component', () => { + const ngModule = ngMocks.guts(TARGET2, Target1Component); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'c')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Component, 'c')).toBeTruthy(); + } + }); + + it('mocks directive', () => { + const ngModule = ngMocks.guts(TARGET2, Target1Directive); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'd')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Directive, 'd')).toBeTruthy(); + } + }); + + it('mocks pipe', () => { + const ngModule = ngMocks.guts(TARGET2, Target1Pipe); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isNgDef(ngModule.declarations[0], 'p')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Pipe, 'p')).toBeTruthy(); + } + }); + + it('mocks service', () => { + const ngModule = ngMocks.guts(TARGET2, Target1Service); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + deps: [Injector], + provide: Target1Service, + useFactory: jasmine.anything(), + }); + } + }); + + it('mocks tokens', () => { + const ngModule = ngMocks.guts(TARGET2, { provide: TARGET1, useValue: 123 }); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + provide: TARGET1, + useValue: 0, + }); + } + }); + + it('skips existing module with providers', () => { + const ngModule = ngMocks.guts(TARGET2, [ + { + ngModule: Target1Module, + providers: [], + }, + { + ngModule: Target1Module, + providers: [], + }, + ]); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(isNgDef(ngModule.imports[0].ngModule, 'm')).toBeTruthy(); + expect(isMockedNgDefOf(ngModule.imports[0].ngModule, Target1Module, 'm')).toBeTruthy(); + } + }); + + it('skips existing module', () => { + const ngModule = ngMocks.guts(Target1Module, [Target1Module, Target1Module]); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(ngModule.imports[0]).toBe(Target1Module); + } + }); + + it('skips 2nd kept module', () => { + const ngModule = ngMocks.guts(Target2Module, [Target1Module, Target1Module]); + expect(ngModule.imports?.length).toEqual(2); + if (ngModule.imports) { + expect(ngModule.imports[0]).toBe(CommonModule); + expect(ngModule.imports[1]).toBe(Target2Module); + } + }); + + it('skips 2nd mocked module', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Module, Target1Module]); + expect(ngModule.imports?.length).toEqual(2); + if (ngModule.imports) { + expect(ngModule.imports[0]).toBe(CommonModule); + expect(isMockedNgDefOf(ngModule.imports[1], Target2Module, 'm')).toBeTruthy(); + } + }); + + it('skips 2nd nested mocked module', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Module, Target3Module]); + expect(ngModule.imports?.length).toEqual(2); + if (ngModule.imports) { + expect(ngModule.imports[0]).toBe(CommonModule); + expect(isMockedNgDefOf(ngModule.imports[1], Target2Module, 'm')).toBeTruthy(); + } + }); + + it('skips 2nd kept module with providers', () => { + const ngModule = ngMocks.guts(Target1Module, [ + { + ngModule: Target1Module, + providers: [], + }, + { + ngModule: Target1Module, + providers: [], + }, + ]); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(ngModule.imports[0].ngModule).toBe(Target1Module); + } + }); + + it('skips 2nd mocked module with providers', () => { + const ngModule = ngMocks.guts(TARGET1, [ + { + ngModule: Target1Module, + providers: [], + }, + { + ngModule: Target1Module, + providers: [], + }, + ]); + expect(ngModule.imports?.length).toEqual(1); + if (ngModule.imports) { + expect(isMockedNgDefOf(ngModule.imports[0].ngModule, Target1Module, 'm')).toBeTruthy(); + } + }); + + it('skips 2nd kept component', () => { + const ngModule = ngMocks.guts(Target1Component, [Target1Component, Target1Component]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(ngModule.declarations[0]).toBe(Target1Component); + } + }); + + it('skips 2nd mocked component', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Component, Target1Component]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Component, 'c')).toBeTruthy(); + } + }); + + it('skips 2nd kept directive', () => { + const ngModule = ngMocks.guts(Target1Directive, [Target1Directive, Target1Directive]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(ngModule.declarations[0]).toBe(Target1Directive); + } + }); + + it('skips 2nd mocked directive', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Directive, Target1Directive]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Directive, 'd')).toBeTruthy(); + } + }); + + it('skips 2nd kept pipe', () => { + const ngModule = ngMocks.guts(Target1Pipe, [Target1Pipe, Target1Pipe]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(ngModule.declarations[0]).toBe(Target1Pipe); + } + }); + + it('skips 2nd mocked pipe', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Pipe, Target1Pipe]); + expect(ngModule.declarations?.length).toEqual(1); + if (ngModule.declarations) { + expect(isMockedNgDefOf(ngModule.declarations[0], Target1Pipe, 'p')).toBeTruthy(); + } + }); + + it('skips 2nd kept service', () => { + const ngModule = ngMocks.guts(Target1Service, [Target1Service, Target1Service]); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toBe(Target1Service); + } + }); + + it('skips 2nd mocked service', () => { + const ngModule = ngMocks.guts(TARGET1, [Target1Service, Target1Service]); + expect(ngModule.providers?.length).toEqual(1); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + deps: [Injector], + provide: Target1Service, + useFactory: jasmine.anything(), + }); + } + }); + + it('skips 2nd kept token', () => { + const ngModule = ngMocks.guts(TARGET1, [ + { + provide: TARGET1, + useValue: 1, + }, + { + provide: TARGET1, + useValue: 2, + }, + ]); + expect(ngModule.providers?.length).toEqual(2); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + provide: TARGET1, + useValue: 1, + }); + expect(ngModule.providers[1]).toEqual({ + provide: TARGET1, + useValue: 2, + }); + } + }); + + it('skips 2nd mocked token', () => { + const ngModule = ngMocks.guts(TARGET2, [ + { + provide: TARGET1, + useValue: 1, + }, + { + provide: TARGET1, + useValue: 2, + }, + ]); + expect(ngModule.providers?.length).toEqual(2); + if (ngModule.providers) { + expect(ngModule.providers[0]).toEqual({ + provide: TARGET1, + useValue: 0, + }); + expect(ngModule.providers[1]).toEqual({ + provide: TARGET1, + useValue: 0, + }); + } + }); + it('skips 2nd mocked multi token', () => { + const ngModule = ngMocks.guts(TARGET2, [ + { + multi: true, + provide: TARGET1, + useValue: 1, + }, + { + multi: true, + provide: TARGET1, + useValue: 2, + }, + ]); + expect(ngModule.providers?.length).toEqual(0); + }); +}); diff --git a/lib/mock-helper/mock-helper.guts.ts b/lib/mock-helper/mock-helper.guts.ts new file mode 100644 index 0000000000..c9065b6c3e --- /dev/null +++ b/lib/mock-helper/mock-helper.guts.ts @@ -0,0 +1,158 @@ +// tslint:disable:no-default-export + +import { core } from '@angular/compiler'; +import { TestModuleMetadata } from '@angular/core/testing'; + +import { flatten, isNgDef, isNgInjectionToken, isNgModuleDefWithProviders } from '../common/lib'; +import { ngModuleResolver } from '../common/reflect'; +import { MockComponent } from '../mock-component/mock-component'; +import { MockDirective } from '../mock-directive/mock-directive'; +import { MockModule, MockProvider } from '../mock-module/mock-module'; +import { MockPipe } from '../mock-pipe/mock-pipe'; + +export default (keep: any, mock?: any): TestModuleMetadata => { + const declarations: any[] = []; + const imports: any[] = []; + const providers: any[] = []; + + const keepFlat: any[] = flatten(keep); + const mockFlat: any[] = mock ? flatten(mock) : []; + const skip: any[] = []; + + const resolve = (def: any, skipDestruction = true): void => { + if (!def) { + return; + } + + if (isNgModuleDefWithProviders(def)) { + if (skip.indexOf(def.ngModule) !== -1) { + return; + } + skip.push(def.ngModule); + + imports.push(keepFlat.indexOf(def.ngModule) === -1 ? MockModule(def) : def); + return; + } + + if (isNgDef(def, 'm') && keepFlat.indexOf(def) !== -1) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + imports.push(def); + return; + } + + if (isNgDef(def, 'm') && skipDestruction && keepFlat.indexOf(def) === -1) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + imports.push(MockModule(def)); + return; + } + + if (isNgDef(def, 'm') && keepFlat.indexOf(def) === -1) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + let meta: core.NgModule; + try { + meta = ngModuleResolver.resolve(def); + } catch (e) { + /* istanbul ignore next */ + throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); + } + + for (const toMock of flatten([meta.declarations, meta.imports, meta.providers])) { + resolve(toMock); + } + return; + } + + if (isNgDef(def, 'c')) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + declarations.push(keepFlat.indexOf(def) === -1 ? MockComponent(def) : def); + return; + } + + if (isNgDef(def, 'd')) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + declarations.push(keepFlat.indexOf(def) === -1 ? MockDirective(def) : def); + return; + } + + if (isNgDef(def, 'p')) { + if (skip.indexOf(def) !== -1) { + return; + } + skip.push(def); + + declarations.push(keepFlat.indexOf(def) === -1 ? MockPipe(def) : def); + return; + } + + const provider = typeof def === 'object' && def.provide ? def.provide : def; + if (!isNgInjectionToken(provider) && skip.indexOf(provider) !== -1) { + return; + } + skip.push(provider); + const providerDef = keepFlat.indexOf(provider) === -1 ? MockProvider(def) : def; + if (providerDef) { + providers.push(providerDef); + } + }; + + for (const def of mockFlat) { + resolve(def, false); + } + + for (const def of keepFlat) { + if (skip.indexOf(def) !== -1) { + continue; + } + + if (isNgDef(def, 'm')) { + imports.push(def); + continue; + } + + if (isNgDef(def, 'c')) { + declarations.push(def); + continue; + } + + if (isNgDef(def, 'd')) { + declarations.push(def); + continue; + } + + if (isNgDef(def, 'p')) { + declarations.push(def); + continue; + } + + if (isNgInjectionToken(def)) { + continue; + } + providers.push(def); + } + + return { + declarations, + imports, + providers, + }; +}; diff --git a/lib/mock-helper/mock-helper.input.ts b/lib/mock-helper/mock-helper.input.ts new file mode 100644 index 0000000000..660491b05b --- /dev/null +++ b/lib/mock-helper/mock-helper.input.ts @@ -0,0 +1,47 @@ +// tslint:disable:no-default-export no-default-import + +import { core } from '@angular/compiler'; + +import { directiveResolver } from '../common/reflect'; +import { MockedDebugElement } from '../mock-render'; + +import get from './mock-helper.get'; + +const defaultNotFoundValue = {}; // simulating Symbol + +export default (...args: any[]) => { + const el: MockedDebugElement = args[0]; + const sel: string = args[1]; + const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; + + for (const token of el.providerTokens) { + let meta: core.Directive; + try { + meta = directiveResolver.resolve(token); + } catch (e) { + /* istanbul ignore next */ + throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); + } + + const { inputs } = meta; + /* istanbul ignore if */ + if (!inputs) { + continue; + } + for (const inputDef of inputs) { + const [prop, alias = ''] = inputDef.split(':', 2).map(v => v.trim()); + if (!alias && prop !== sel) { + continue; + } + if (alias && alias !== sel) { + continue; + } + const directive: any = get(el, token); + return directive[prop]; + } + } + if (notFoundValue !== defaultNotFoundValue) { + return notFoundValue; + } + throw new Error(`Cannot find ${sel} input via ngMocks.input`); +}; diff --git a/lib/mock-helper/mock-helper.output.ts b/lib/mock-helper/mock-helper.output.ts new file mode 100644 index 0000000000..1e25029175 --- /dev/null +++ b/lib/mock-helper/mock-helper.output.ts @@ -0,0 +1,47 @@ +// tslint:disable:no-default-export no-default-import + +import { core } from '@angular/compiler'; + +import { directiveResolver } from '../common/reflect'; +import { MockedDebugElement } from '../mock-render'; + +import get from './mock-helper.get'; + +const defaultNotFoundValue = {}; // simulating Symbol + +export default (...args: any[]) => { + const el: MockedDebugElement = args[0]; + const sel: string = args[1]; + const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; + + for (const token of el.providerTokens) { + let meta: core.Directive; + try { + meta = directiveResolver.resolve(token); + } catch (e) { + /* istanbul ignore next */ + throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); + } + + const { outputs } = meta; + /* istanbul ignore if */ + if (!outputs) { + continue; + } + for (const outputDef of outputs) { + const [prop, alias = ''] = outputDef.split(':', 2).map(v => v.trim()); + if (!alias && prop !== sel) { + continue; + } + if (alias && alias !== sel) { + continue; + } + const directive: any = get(el, token); + return directive[prop]; + } + } + if (notFoundValue !== defaultNotFoundValue) { + return notFoundValue; + } + throw new Error(`Cannot find ${sel} output via ngMocks.output`); +}; diff --git a/lib/mock-helper/mock-helper.reset.ts b/lib/mock-helper/mock-helper.reset.ts new file mode 100644 index 0000000000..b9b4faa3cc --- /dev/null +++ b/lib/mock-helper/mock-helper.reset.ts @@ -0,0 +1,13 @@ +// tslint:disable:no-default-export + +import { ngMocksUniverse } from '../common/ng-mocks-universe'; + +export default (): void => { + ngMocksUniverse.builder = new Map(); + ngMocksUniverse.cacheMocks = new Map(); + ngMocksUniverse.cacheProviders = new Map(); + ngMocksUniverse.config = new Map(); + ngMocksUniverse.global = new Map(); + ngMocksUniverse.flags = new Set(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']); + ngMocksUniverse.touches = new Set(); +}; diff --git a/lib/mock-helper/mock-helper.stub.ts b/lib/mock-helper/mock-helper.stub.ts new file mode 100644 index 0000000000..164535803c --- /dev/null +++ b/lib/mock-helper/mock-helper.stub.ts @@ -0,0 +1,17 @@ +// tslint:disable:no-default-export + +import { MockedFunction, mockServiceHelper } from '../mock-service/mock-service'; + +export default (instance: any, override: any, style?: 'get' | 'set'): T => { + if (typeof override === 'string') { + return mockServiceHelper.mock(instance, override, style); + } + for (const key of Object.getOwnPropertyNames(override)) { + const def = Object.getOwnPropertyDescriptor(override, key); + /* istanbul ignore else */ + if (def) { + Object.defineProperty(instance, key, def); + } + } + return instance; +}; diff --git a/lib/mock-helper/mock-helper.ts b/lib/mock-helper/mock-helper.ts index 635a83210a..2954c23c4d 100644 --- a/lib/mock-helper/mock-helper.ts +++ b/lib/mock-helper/mock-helper.ts @@ -1,30 +1,24 @@ -/* tslint:disable:variable-name unified-signatures */ +// tslint:disable:variable-name unified-signatures no-default-import -import { core } from '@angular/compiler'; -import { EventEmitter } from '@angular/core'; -import { getTestBed, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; +import { EventEmitter, InjectionToken, Provider } from '@angular/core'; +import { TestModuleMetadata } from '@angular/core/testing'; -import { AbstractType, getSourceOfMock, Type } from '../common'; -import { ngMocksUniverse } from '../common/ng-mocks-universe'; -import { directiveResolver } from '../common/reflect'; +import { AbstractType, AnyType, NgModuleWithProviders, Type } from '../common'; import { MockedDebugElement, MockedDebugNode } from '../mock-render'; -import { MockedFunction, mockServiceHelper } from '../mock-service'; - -function nestedCheck( - result: T[], - node: MockedDebugNode & { childNodes?: MockedDebugNode[] }, - callback: (node: MockedDebugNode) => undefined | T -) { - const element = callback(node); - if (element) { - result.push(element); - } - const childNodes = node.childNodes ? node.childNodes : []; - childNodes.forEach(childNode => { - nestedCheck(result, childNode, callback); - }); -} +import { MockedFunction } from '../mock-service'; + +import ngMocksFaster from './mock-helper.faster'; +import ngMocksFind from './mock-helper.find'; +import ngMocksFindAll from './mock-helper.findAll'; +import ngMocksFindInstance from './mock-helper.findInstance'; +import ngMocksFindInstances from './mock-helper.findInstances'; +import ngMocksFlushTestBed from './mock-helper.flushTestBed'; +import ngMocksGet from './mock-helper.get'; +import ngMocksGuts from './mock-helper.guts'; +import ngMocksInput from './mock-helper.input'; +import ngMocksOutput from './mock-helper.output'; +import ngMocksReset from './mock-helper.reset'; +import ngMocksStub from './mock-helper.stub'; /* istanbul ignore next */ /** @@ -95,8 +89,6 @@ export const MockHelper: { }, }; -const defaultNotFoundValue = {}; // simulating Symbol - export const ngMocks: { faster(): void; @@ -122,6 +114,16 @@ export const ngMocks: { get(debugNode: MockedDebugNode, directive: Type): T; get(debugNode: MockedDebugNode, directive: Type, notFoundValue: D): D | T; + guts( + keep: AnyType | InjectionToken | Array | InjectionToken>, + mock?: + | AnyType + | InjectionToken + | NgModuleWithProviders + | Provider + | Array | InjectionToken | NgModuleWithProviders | Provider> + ): TestModuleMetadata; + input(debugNode: MockedDebugNode, input: string): T; input(debugNode: MockedDebugNode, input: string, notFoundValue: D): D | T; @@ -133,214 +135,16 @@ export const ngMocks: { stub(instance: I, name: keyof I, style?: 'get' | 'set'): T; stub(instance: I, overrides: Partial): I; } = { - faster: () => { - beforeAll(() => { - if (ngMocksUniverse.global.has('bullet:customized')) { - TestBed.resetTestingModule(); - } - ngMocksUniverse.global.set('bullet', true); - }); - - afterEach(() => { - ngMocks.flushTestBed(); - for (const fixture of (getTestBed() as any)._activeFixtures || /* istanbul ignore next */ []) { - fixture.destroy(); - } - }); - - afterAll(() => { - ngMocksUniverse.global.delete('bullet'); - if (ngMocksUniverse.global.has('bullet:reset')) { - TestBed.resetTestingModule(); - } - }); - }, - - find: (...args: any[]) => { - const el: MockedDebugElement = args[0]; - const sel: string | Type = args[1]; - const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - - const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); - const result = el.query(term); - if (result) { - return result; - } - if (notFoundValue !== defaultNotFoundValue) { - return notFoundValue; - } - throw new Error(`Cannot find an element via ngMocks.find(${typeof sel === 'string' ? sel : sel.name})`); - }, - - findAll: (el: MockedDebugElement, sel: any) => { - const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel)); - return el.queryAll(term); - }, - - findInstance: (...args: any[]) => { - const el: MockedDebugElement = args[0]; - const sel: Type = args[1]; - const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - - const result = ngMocks.findInstances(el, getSourceOfMock(sel)); - if (result.length) { - return result[0]; - } - if (notFoundValue !== defaultNotFoundValue) { - return notFoundValue; - } - throw new Error(`Cannot find ${sel.name} directive via ngMocks.findInstance`); - }, - - findInstances: (el: MockedDebugNode, sel: Type): T[] => { - const result: T[] = []; - nestedCheck(result, el, node => { - try { - return node.injector.get(getSourceOfMock(sel)); - } catch (error) { - return undefined; - } - }); - return result; - }, - - get: (...args: any[]) => { - const el: MockedDebugElement = args[0]; - const sel: Type = args[1]; - const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - let notFound = false; - - // Looking for related attribute directive. - try { - return el.injector.get(getSourceOfMock(sel)); - } catch (error) { - // looks like the directive is structural. - } - - // Looking for related structural directive. - // It's located as prev node. - const prevNode = el.nativeNode.previousSibling; - if (!prevNode || prevNode.nodeName !== '#comment') { - notFound = true; - } - const matches = notFound || !el || !el.parent ? [] : el.parent.queryAllNodes(node => node.nativeNode === prevNode); - if (matches.length === 0) { - notFound = true; - } - const matchedNode = matches[0]; - try { - return matchedNode.injector.get(getSourceOfMock(sel)); - } catch (error) { - notFound = true; - } - if (notFound && notFoundValue !== defaultNotFoundValue) { - return notFoundValue; - } - throw new Error(`Cannot find ${sel.name} directive via ngMocks.get`); - }, - - input: (...args: any[]) => { - const el: MockedDebugElement = args[0]; - const sel: string = args[1]; - const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - - for (const token of el.providerTokens) { - let meta: core.Directive; - try { - meta = directiveResolver.resolve(token); - } catch (e) { - /* istanbul ignore next */ - throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); - } - - const { inputs } = meta; - /* istanbul ignore if */ - if (!inputs) { - continue; - } - for (const inputDef of inputs) { - const [prop, alias = ''] = inputDef.split(':', 2).map(v => v.trim()); - if (!alias && prop !== sel) { - continue; - } - if (alias && alias !== sel) { - continue; - } - const directive: any = ngMocks.get(el, token); - return directive[prop]; - } - } - if (notFoundValue !== defaultNotFoundValue) { - return notFoundValue; - } - throw new Error(`Cannot find ${sel} input via ngMocks.input`); - }, - - output: (...args: any[]) => { - const el: MockedDebugElement = args[0]; - const sel: string = args[1]; - const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue; - - for (const token of el.providerTokens) { - let meta: core.Directive; - try { - meta = directiveResolver.resolve(token); - } catch (e) { - /* istanbul ignore next */ - throw new Error('ng-mocks is not in JIT mode and cannot resolve declarations'); - } - - const { outputs } = meta; - /* istanbul ignore if */ - if (!outputs) { - continue; - } - for (const outputDef of outputs) { - const [prop, alias = ''] = outputDef.split(':', 2).map(v => v.trim()); - if (!alias && prop !== sel) { - continue; - } - if (alias && alias !== sel) { - continue; - } - const directive: any = ngMocks.get(el, token); - return directive[prop]; - } - } - if (notFoundValue !== defaultNotFoundValue) { - return notFoundValue; - } - throw new Error(`Cannot find ${sel} output via ngMocks.output`); - }, - - stub: (instance: any, override: any, style?: 'get' | 'set'): T => { - if (typeof override === 'string') { - return mockServiceHelper.mock(instance, override, style); - } - for (const key of Object.getOwnPropertyNames(override)) { - const def = Object.getOwnPropertyDescriptor(override, key); - /* istanbul ignore else */ - if (def) { - Object.defineProperty(instance, key, def); - } - } - return instance; - }, - - flushTestBed(): void { - const testBed: any = getTestBed(); - testBed._instantiated = false; - testBed._moduleFactory = undefined; - testBed._testModuleRef = null; - }, - - reset(): void { - ngMocksUniverse.builder = new Map(); - ngMocksUniverse.cacheMocks = new Map(); - ngMocksUniverse.cacheProviders = new Map(); - ngMocksUniverse.config = new Map(); - ngMocksUniverse.global = new Map(); - ngMocksUniverse.flags = new Set(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']); - ngMocksUniverse.touches = new Set(); - }, + faster: ngMocksFaster, + find: ngMocksFind, + findAll: ngMocksFindAll, + findInstance: ngMocksFindInstance, + findInstances: ngMocksFindInstances, + flushTestBed: ngMocksFlushTestBed, + get: ngMocksGet, + guts: ngMocksGuts, + input: ngMocksInput, + output: ngMocksOutput, + reset: ngMocksReset, + stub: ngMocksStub, }; diff --git a/lib/mock-module/mock-module.spec.ts b/lib/mock-module/mock-module.spec.ts index 2d894988f3..a1016bb2cf 100644 --- a/lib/mock-module/mock-module.spec.ts +++ b/lib/mock-module/mock-module.spec.ts @@ -1,6 +1,14 @@ import { CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { APP_INITIALIZER, ApplicationModule, Component, InjectionToken, NgModule } from '@angular/core'; +import { + APP_INITIALIZER, + ApplicationModule, + Component, + FactoryProvider, + InjectionToken, + Injector, + NgModule, +} from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -208,10 +216,15 @@ describe('MockProvider', () => { }) class CustomTokenModule {} - it('should skip tokens in a mocked module', () => { + it('should skip multi tokens in a mocked module', () => { const mock = MockModule(CustomTokenModule); const def = ngModuleResolver.resolve(mock); - expect(def.providers).toEqual([]); + expect(def.providers).toEqual([ + { + provide: CUSTOM_TOKEN, + useValue: '', + }, + ]); }); it('should return undefined on any token', () => { @@ -219,6 +232,44 @@ describe('MockProvider', () => { expect(MockProvider(HTTP_INTERCEPTORS)).toBeUndefined(); expect(MockProvider(APP_INITIALIZER)).toBeUndefined(); }); + + it('should return default value on primitives', () => { + expect(MockProvider({ provide: CUSTOM_TOKEN, useValue: undefined })).toEqual({ + provide: CUSTOM_TOKEN, + useValue: undefined, + }); + expect(MockProvider({ provide: CUSTOM_TOKEN, useValue: 123 })).toEqual({ + provide: CUSTOM_TOKEN, + useValue: 0, + }); + expect(MockProvider({ provide: CUSTOM_TOKEN, useValue: true })).toEqual({ + provide: CUSTOM_TOKEN, + useValue: false, + }); + expect(MockProvider({ provide: CUSTOM_TOKEN, useValue: 'true' })).toEqual({ + provide: CUSTOM_TOKEN, + useValue: '', + }); + expect(MockProvider({ provide: CUSTOM_TOKEN, useValue: null })).toEqual({ + provide: CUSTOM_TOKEN, + useValue: null, + }); + const mock: FactoryProvider = MockProvider({ + provide: CUSTOM_TOKEN, + useValue: { + func: () => undefined, + test: 123, + }, + }) as any; + expect(mock).toEqual({ + deps: [Injector], + provide: CUSTOM_TOKEN, + useFactory: jasmine.anything(), + }); + expect(mock.useFactory(null)).toEqual({ + func: jasmine.anything(), + }); + }); }); describe('MockModuleWithProviders', () => { diff --git a/lib/mock-module/mock-module.ts b/lib/mock-module/mock-module.ts index 8d64f82116..3ebed34842 100644 --- a/lib/mock-module/mock-module.ts +++ b/lib/mock-module/mock-module.ts @@ -8,6 +8,7 @@ import { flatten, getMockedNgDefOf, isNgDef, + isNgInjectionToken, isNgModuleDefWithProviders, Mock, MockOf, @@ -40,7 +41,30 @@ export function MockProvider(provider: any): Provider | undefined { // The main problem is that providing undefined to HTTP_INTERCEPTORS and others breaks their code. // If a testing module / component requires omitted tokens then they should be provided manually // during creation of TestBed module. - if (typeof provide === 'object' && provide.ngMetadataName === 'InjectionToken') { + if (isNgInjectionToken(provide) && provider.multi) { + return undefined; + } + // if a token has a primitive type, we can return its initial state. + if (isNgInjectionToken(provide) && Object.keys(provider).indexOf('useValue') !== -1) { + return provider.useValue && typeof provider.useValue === 'object' + ? mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () => + MockService(provider.useValue) + ) + : { + provide, + useValue: + typeof provider.useValue === 'boolean' + ? false + : typeof provider.useValue === 'number' + ? 0 + : typeof provider.useValue === 'string' + ? '' + : provider.useValue === null + ? null + : undefined, + }; + } + if (isNgInjectionToken(provide)) { return undefined; } diff --git a/tests/mock-builder-keeps-application-module/fixtures.modules.ts b/tests/mock-builder-keeps-application-module/fixtures.modules.ts index dd7e1ea0d1..3c43d94ec6 100644 --- a/tests/mock-builder-keeps-application-module/fixtures.modules.ts +++ b/tests/mock-builder-keeps-application-module/fixtures.modules.ts @@ -1,4 +1,4 @@ -import { APP_ID, APP_INITIALIZER, InjectionToken, NgModule } from '@angular/core'; +import { APP_INITIALIZER, InjectionToken, NgModule } from '@angular/core'; import { TargetComponent } from './fixtures.components'; @@ -12,10 +12,6 @@ export const TARGET_TOKEN = new InjectionToken('TARGET_TOKEN'); provide: TARGET_TOKEN, useValue: 'TARGET_TOKEN', }, - { - provide: APP_ID, - useValue: 'random', - }, { multi: true, provide: APP_INITIALIZER, diff --git a/tests/mock-builder-keeps-application-module/test.spec.ts b/tests/mock-builder-keeps-application-module/test.spec.ts index 19826c493f..362f105e5e 100644 --- a/tests/mock-builder-keeps-application-module/test.spec.ts +++ b/tests/mock-builder-keeps-application-module/test.spec.ts @@ -25,7 +25,7 @@ describe('MockBuilderKeepsApplicationModule:mock', () => { const fixture = MockRender(TargetComponent); const element = ngMocks.find(fixture.debugElement, TargetComponent); expect(element).toBeDefined(); - expect(() => TestBed.get(TARGET_TOKEN)).toThrow(); + expect(TestBed.get(TARGET_TOKEN)).toEqual(''); if (parseInt(VERSION.major, 10) < 9) { // somehow ivy doesn't provide APP_INITIALIZER out of the box and this assertion fails. // our mock logic skips all multi tokens therefore this one isn't present anymore.