From f33a1320a8cc98dd42dacb6c6cdcd5a190218580 Mon Sep 17 00:00:00 2001 From: MG Date: Wed, 30 Dec 2020 21:58:14 +0100 Subject: [PATCH] feat(mock-render): renders everything closes #266 --- README.md | 118 +++++++--- examples/MockObservable/test.spec.ts | 9 +- examples/MockProvider/test.spec.ts | 11 +- examples/TestHttpInterceptor/test.spec.ts | 14 +- examples/TestHttpRequest/test.spec.ts | 10 +- examples/TestLifecycleHooks/test.spec.ts | 8 +- .../TestLifecycleHooks/test.string.spec.ts | 5 +- .../test.type-without-params.spec.ts | 5 +- examples/TestMultiToken/test.spec.ts | 6 +- examples/TestProvider/test.spec.ts | 5 +- .../TestProviderWithDependencies/test.spec.ts | 10 +- .../TestProviderWithUseClass/test.spec.ts | 12 +- .../TestProviderWithUseExisting/test.spec.ts | 12 +- .../TestProviderWithUseFactory/test.spec.ts | 5 +- .../TestProviderWithUseValue/test.spec.ts | 8 +- examples/TestRoute/test.spec.ts | 14 +- examples/TestRoutingGuard/test.spec.ts | 14 +- examples/TestRoutingResolver/test.spec.ts | 10 +- examples/TestToken/test.spec.ts | 13 +- lib/common/core.helpers.ts | 30 +-- ...ect.body.ts => core.reflect.body-catch.ts} | 0 lib/common/core.reflect.body-global.ts | 11 + lib/common/core.reflect.directive-resolve.ts | 4 +- lib/common/core.reflect.directive.ts | 11 +- lib/common/core.reflect.module-resolve.ts | 4 +- lib/common/core.reflect.module.ts | 11 +- lib/common/core.reflect.pipe-resolve.ts | 4 +- lib/common/core.reflect.pipe.ts | 11 +- lib/common/core.tokens.ts | 6 +- lib/common/decorate.mock.ts | 12 +- lib/common/func.is-ng-def.ts | 14 +- lib/common/func.is-ng-type.ts | 4 +- lib/common/mock-control-value-accessor.ts | 6 +- lib/mock-component/mock-component.ts | 2 +- lib/mock-declaration/mock-declaration.spec.ts | 26 ++- lib/mock-directive/mock-directive.ts | 2 +- lib/mock-module/mock-module.ts | 4 +- lib/mock-pipe/mock-pipe.ts | 4 +- lib/mock-provider/mock-provider.ts | 6 +- lib/mock-render/func.generate-template.ts | 52 +++++ lib/mock-render/func.install-prop-reader.ts | 37 +++ lib/mock-render/mock-render.spec.ts | 11 +- lib/mock-render/mock-render.ts | 217 +++++++++--------- lib/mock-service/mock-service.ts | 8 +- tests-failures/mock-render-token.ts | 22 ++ tests/issue-266/test.spec.ts | 117 ++++++++++ tests/mock-render-tokens/test.spec.ts | 33 +++ 47 files changed, 664 insertions(+), 294 deletions(-) rename lib/common/{core.reflect.body.ts => core.reflect.body-catch.ts} (100%) create mode 100644 lib/common/core.reflect.body-global.ts create mode 100644 lib/mock-render/func.generate-template.ts create mode 100644 lib/mock-render/func.install-prop-reader.ts create mode 100644 tests-failures/mock-render-token.ts create mode 100644 tests/issue-266/test.spec.ts create mode 100644 tests/mock-render-tokens/test.spec.ts diff --git a/README.md b/README.md index 3a42d516c4..11e9966975 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,7 @@ TestBed.configureTestingModule({ // ... ], providers: [ - LoginService, - DataService, + SearchService, // ... ], }); @@ -203,14 +202,36 @@ TestBed.configureTestingModule({ // ... ], providers: [ - MockProvider(LoginService), - MockProvider(DataService), + MockProvider(SearchService), // ... ], }); ``` -Profit. Now we can forget about noise of child dependencies. +If you have noticed `search$ | async` in the template, you made the right assumption: +we need to provide a fake observable stream within the mock `SearchService` to avoid failures +like [`Cannot read property 'pipe' of undefined`](#how-to-fix-typeerror-cannot-read-property-subscribe-of-undefined), +when the component tries to execute `this.search$ = this.searchService.result$.pipe(...)` in `ngOnInit`. + +For example, we can implement it via [`MockInstance`](#mockinstance): + +```ts +beforeEach(() => + MockInstance(SearchService, () => ({ + result$: EMPTY, + })), +); +``` + +or to set it as default mock behavior for all tests via adding [`ngMocks.defaultMock`](#ngmocksdefaultmock) in the `test.ts` file: + +```ts +ngMocks.defaultMock(SearchService, () => ({ + result$: EMPTY, +})); +``` + +Profit. Now, we can forget about noise of child dependencies. Nevertheless, if we count lines of mock declarations, we see that there are a lot of them, and looks like here might be dozens more for big @@ -274,7 +295,7 @@ beforeEach(() => { return MockBuilder(AppBaseComponent, AppBaseModule) .mock(TranslatePipe, v => `translated:${v}`) .mock(SearchService, { - search: of([]), + result$: EMPTY, }); }); ``` @@ -331,14 +352,14 @@ a type of `MockedComponent` and provides: - the same `selector` - the same `Inputs` and `Outputs` with alias support -- templates are pure `ng-content` tags to allow transclusion -- supports `@ContentChild` with an `$implicit` context +- templates with pure `ng-content` tags to allow transclusion +- support of `@ContentChild` with an `$implicit` context - `__render('id', $implicit, variables)` - renders a template - `__hide('id')` - hides a rendered template -- supports [`FormsModule`, `ReactiveFormsModule` and `ControlValueAccessor`](#how-to-mock-form-controls) +- support of [`FormsModule`, `ReactiveFormsModule` and `ControlValueAccessor`](#how-to-mock-form-controls) - `__simulateChange()` - calls `onChanged` on the mock component bound to a `FormControl` - `__simulateTouch()` - calls `onTouched` on the mock component bound to a `FormControl` -- supports `exportAs` +- support of `exportAs` Let's pretend that in our Angular application `TargetComponent` depends on a child component of `DependencyComponent` and we want to use its mock object in a test. @@ -972,17 +993,14 @@ describe('MockProvider', () => { it('uses mock providers', () => { // overriding the token's data that does affect the provided token. mockObj.value = 321; - const fixture = TestBed.createComponent(TargetComponent); - fixture.detectChanges(); + const fixture = MockRender(TargetComponent); expect( - fixture.debugElement.injector.get(Dependency1Service).echo(), + fixture.point.injector.get(Dependency1Service).echo(), ).toBeUndefined(); expect( - fixture.debugElement.injector.get(Dependency2Service).echo(), + fixture.point.injector.get(Dependency2Service).echo(), ).toBeUndefined(); - expect(fixture.debugElement.injector.get(OBJ_TOKEN)).toBe( - mockObj, - ); + expect(fixture.point.injector.get(OBJ_TOKEN)).toBe(mockObj); expect(fixture.nativeElement.innerHTML).not.toContain('"target"'); expect(fixture.nativeElement.innerHTML).toContain('"d2:mock"'); expect(fixture.nativeElement.innerHTML).toContain('"mock token"'); @@ -1290,8 +1308,12 @@ describe('MockObservable', () => { // Checking that a sibling method has been replaced // with a mock object too. - expect(TestBed.inject(TargetService).getValue$).toBeDefined(); - expect(TestBed.inject(TargetService).getValue$()).toBeUndefined(); + expect( + fixture.point.injector.get(TargetService).getValue$, + ).toBeDefined(); + expect( + fixture.point.injector.get(TargetService).getValue$(), + ).toBeUndefined(); }); }); ``` @@ -2137,11 +2159,16 @@ beforeEach(() => { ### MockRender -`MockRender` is a simple tool that helps with **shallow rendering in Angular tests** -when we want to assert `Inputs`, `Outputs`, `ChildContent` and custom templates. +**Shallow rendering in Angular tests** is provided via `MockRender` function. +`MockRender` helps when we want to assert `Inputs`, `Outputs`, `ChildContent` and custom templates. + +`MockRender` relies on Angular `TestBed` and provides: -The best thing about it is that `MockRender` properly triggers all lifecycle hooks -and allows **to test `ngOnChanges` hook from `OnChanges` interface**. +- shallow rendering of Components, Directives, Services, Tokens +- rendering of custom templates +- support of context providers +- support of all lifecycle hooks (`ngOnInit`, `ngOnChanges` etc) +- support of components without selectors **Please note**, that `MockRender(Component)` is not assignable to `ComponentFixture`. You should use either @@ -2162,16 +2189,48 @@ It happens because `MockRender` generates an additional component to render the desired thing and its interface differs. It returns `MockedComponentFixture` type. The difference is an additional `point` property. -The best thing about it is that `fixture.point.componentInstance` is typed to the component's class instead of `any`. +The best thing about it is that `fixture.point.componentInstance` is typed to the related class instead of `any`. + +```ts +// component +const fixture = MockRender(AppComponent); +fixture.componentInstance; // is a middle component, mostly useless +fixture.point.componentInstance; // an instance of the AppComponent +``` + +```ts +// directive +const fixture = MockRender(AppDirective); +fixture.componentInstance; // is a middle component, mostly useless +fixture.point.componentInstance; // an instance of AppDirective +``` ```ts -const fixture = MockRender(ComponentToRender); +// service +const fixture = MockRender(TranslationService); +fixture.componentInstance; // is a middle component, mostly useless +fixture.point.componentInstance; // an instance of TranslationService +``` + +```ts +// token +const fixture = MockRender(APP_BASE_HREF); +fixture.componentInstance; // is a middle component, mostly useless +fixture.point.componentInstance; // a value of APP_BASE_HREF +``` + +```ts +// custom template +const fixture = MockRender( + ` { // Checking that a sibling method has been replaced // with a mock object too. - expect(TestBed.get(TargetService).getValue$).toBeDefined(); - expect(TestBed.get(TargetService).getValue$()).toBeUndefined(); + expect( + fixture.point.injector.get(TargetService).getValue$, + ).toBeDefined(); + expect( + fixture.point.injector.get(TargetService).getValue$(), + ).toBeUndefined(); }); }); diff --git a/examples/MockProvider/test.spec.ts b/examples/MockProvider/test.spec.ts index 766608d02a..36bc9ab485 100644 --- a/examples/MockProvider/test.spec.ts +++ b/examples/MockProvider/test.spec.ts @@ -6,7 +6,7 @@ import { InjectionToken, } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { MockProvider } from 'ng-mocks'; +import { MockProvider, MockRender } from 'ng-mocks'; const UNK_TOKEN = new InjectionToken('UNK_TOKEN'); const STR_TOKEN = new InjectionToken('STR_TOKEN'); @@ -72,15 +72,14 @@ describe('MockProvider', () => { it('uses mock providers', () => { // overriding the token's data that does affect the provided token. mockObj.value = 321; - const fixture = TestBed.createComponent(TargetComponent); - fixture.detectChanges(); + const fixture = MockRender(TargetComponent); expect( - fixture.debugElement.injector.get(Dependency1Service).echo(), + fixture.point.injector.get(Dependency1Service).echo(), ).toBeUndefined(); expect( - fixture.debugElement.injector.get(Dependency2Service).echo(), + fixture.point.injector.get(Dependency2Service).echo(), ).toBeUndefined(); - expect(fixture.debugElement.injector.get(OBJ_TOKEN)).toBe( + expect(fixture.point.injector.get(OBJ_TOKEN)).toBe( mockObj as any, ); expect(fixture.nativeElement.innerHTML).not.toContain('"target"'); diff --git a/examples/TestHttpInterceptor/test.spec.ts b/examples/TestHttpInterceptor/test.spec.ts index 9756da7fac..20b02c66a6 100644 --- a/examples/TestHttpInterceptor/test.spec.ts +++ b/examples/TestHttpInterceptor/test.spec.ts @@ -12,8 +12,11 @@ import { HttpTestingController, } from '@angular/common/http/testing'; import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, NG_MOCKS_INTERCEPTORS } from 'ng-mocks'; +import { + MockBuilder, + MockRender, + NG_MOCKS_INTERCEPTORS, +} from 'ng-mocks'; import { Observable } from 'rxjs'; // An interceptor we want to test. @@ -88,8 +91,11 @@ describe('TestHttpInterceptor', () => { }); it('triggers interceptor', () => { - const client: HttpClient = TestBed.get(HttpClient); - const httpMock: HttpTestingController = TestBed.get( + const fixture = MockRender(''); + const client: HttpClient = fixture.debugElement.injector.get( + HttpClient, + ); + const httpMock: HttpTestingController = fixture.debugElement.injector.get( HttpTestingController, ); diff --git a/examples/TestHttpRequest/test.spec.ts b/examples/TestHttpRequest/test.spec.ts index 3517878d07..088c1247e8 100644 --- a/examples/TestHttpRequest/test.spec.ts +++ b/examples/TestHttpRequest/test.spec.ts @@ -4,8 +4,7 @@ import { HttpTestingController, } from '@angular/common/http/testing'; import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder } from 'ng-mocks'; +import { MockBuilder, MockRender } from 'ng-mocks'; import { Observable } from 'rxjs'; // A service that does http requests. @@ -39,9 +38,12 @@ describe('TestHttpRequest', () => { }); it('sends a request', () => { + const fixture = MockRender(''); // Let's extract the service and http controller for testing. - const service: TargetService = TestBed.get(TargetService); - const httpMock: HttpTestingController = TestBed.get( + const service: TargetService = fixture.debugElement.injector.get( + TargetService, + ); + const httpMock: HttpTestingController = fixture.debugElement.injector.get( HttpTestingController, ); diff --git a/examples/TestLifecycleHooks/test.spec.ts b/examples/TestLifecycleHooks/test.spec.ts index 7362d73328..4be5eed14b 100644 --- a/examples/TestLifecycleHooks/test.spec.ts +++ b/examples/TestLifecycleHooks/test.spec.ts @@ -23,7 +23,9 @@ describe('TestLifecycleHooks', () => { { detectChanges: false }, ); - const service: TargetService = TestBed.get(TargetService); + const service: TargetService = fixture.point.injector.get( + TargetService, + ); // By default nothing should be initialized, but ctor. expect(service.ctor).toHaveBeenCalledTimes(1); // changed @@ -95,7 +97,9 @@ describe('TestLifecycleHooks', () => { const fixture = TestBed.createComponent(TargetComponent); fixture.componentInstance.input = ''; - const service: TargetService = TestBed.get(TargetService); + const service: TargetService = fixture.debugElement.injector.get( + TargetService, + ); // By default nothing should be initialized. expect(service.onChanges).toHaveBeenCalledTimes(0); diff --git a/examples/TestLifecycleHooks/test.string.spec.ts b/examples/TestLifecycleHooks/test.string.spec.ts index 4fba0bcb7e..5577696772 100644 --- a/examples/TestLifecycleHooks/test.string.spec.ts +++ b/examples/TestLifecycleHooks/test.string.spec.ts @@ -1,4 +1,3 @@ -import { TestBed } from '@angular/core/testing'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { @@ -23,7 +22,9 @@ describe('TestLifecycleHooks:string', () => { }, ); - const service: TargetService = TestBed.get(TargetService); + const service: TargetService = fixture.point.injector.get( + TargetService, + ); // By default nothing should be initialized, but ctor. expect(service.ctor).toHaveBeenCalledTimes(1); // changed diff --git a/examples/TestLifecycleHooks/test.type-without-params.spec.ts b/examples/TestLifecycleHooks/test.type-without-params.spec.ts index 5a9d491c13..0da2c4e57f 100644 --- a/examples/TestLifecycleHooks/test.type-without-params.spec.ts +++ b/examples/TestLifecycleHooks/test.type-without-params.spec.ts @@ -1,4 +1,3 @@ -import { TestBed } from '@angular/core/testing'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { @@ -19,7 +18,9 @@ describe('TestLifecycleHooks:type-without-params', () => { detectChanges: false, }); - const service: TargetService = TestBed.get(TargetService); + const service: TargetService = fixture.point.injector.get( + TargetService, + ); // By default nothing should be initialized, but ctor. expect(service.ctor).toHaveBeenCalledTimes(1); // changed diff --git a/examples/TestMultiToken/test.spec.ts b/examples/TestMultiToken/test.spec.ts index 0e2813f34e..dc978f4380 100644 --- a/examples/TestMultiToken/test.spec.ts +++ b/examples/TestMultiToken/test.spec.ts @@ -1,6 +1,5 @@ import { Injectable, InjectionToken, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder } from 'ng-mocks'; +import { MockBuilder, MockRender } from 'ng-mocks'; const TOKEN_MULTI = new InjectionToken('MULTI'); @@ -49,7 +48,8 @@ describe('TestMultiToken', () => { beforeEach(() => MockBuilder(TOKEN_MULTI, TargetModule)); it('creates TOKEN_MULTI', () => { - const tokens = TestBed.get(TOKEN_MULTI); + const tokens = MockRender(TOKEN_MULTI).point + .componentInstance; expect(tokens).toEqual(jasmine.any(Array)); expect(tokens.length).toEqual(4); diff --git a/examples/TestProvider/test.spec.ts b/examples/TestProvider/test.spec.ts index 156fff38cc..b3cfa47519 100644 --- a/examples/TestProvider/test.spec.ts +++ b/examples/TestProvider/test.spec.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder } from 'ng-mocks'; +import { MockBuilder, MockRender } from 'ng-mocks'; // A simple service, might have contained more logic, // but it is redundant for the test demonstration. @@ -18,7 +17,7 @@ describe('TestProvider', () => { beforeEach(() => MockBuilder(TargetService)); it('returns value on echo', () => { - const service = TestBed.get(TargetService); + const service = MockRender(TargetService).point.componentInstance; expect(service.echo()).toEqual(service.value); }); diff --git a/examples/TestProviderWithDependencies/test.spec.ts b/examples/TestProviderWithDependencies/test.spec.ts index 92cd94736f..284c0f1acb 100644 --- a/examples/TestProviderWithDependencies/test.spec.ts +++ b/examples/TestProviderWithDependencies/test.spec.ts @@ -1,6 +1,10 @@ import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, MockInstance, MockReset } from 'ng-mocks'; +import { + MockBuilder, + MockInstance, + MockRender, + MockReset, +} from 'ng-mocks'; // Dependency 1 @Injectable() @@ -66,7 +70,7 @@ describe('TestProviderWithDependencies', () => { it('creates TargetService', () => { // Creates an instance only if all dependencies are present. - const service = TestBed.get(TargetService); + const service = MockRender(TargetService).point.componentInstance; // Let's assert behavior. expect(service.value1).toEqual('mock1'); diff --git a/examples/TestProviderWithUseClass/test.spec.ts b/examples/TestProviderWithUseClass/test.spec.ts index edcd5565f7..90e63db4fe 100644 --- a/examples/TestProviderWithUseClass/test.spec.ts +++ b/examples/TestProviderWithUseClass/test.spec.ts @@ -1,6 +1,10 @@ import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, MockInstance, MockReset } from 'ng-mocks'; +import { + MockBuilder, + MockInstance, + MockRender, + MockReset, +} from 'ng-mocks'; // A service we want to replace via useClass. @Injectable() @@ -66,7 +70,9 @@ describe('TestProviderWithUseClass', () => { afterAll(MockReset); it('respects all dependencies', () => { - const service = TestBed.get(Target1Service); + const service = MockRender< + Target1Service & Partial + >(Target1Service).point.componentInstance; // Let's assert that service has a flag from Target2Service. expect(service.flag).toBeTruthy(); diff --git a/examples/TestProviderWithUseExisting/test.spec.ts b/examples/TestProviderWithUseExisting/test.spec.ts index e29c2cdfbd..d223611975 100644 --- a/examples/TestProviderWithUseExisting/test.spec.ts +++ b/examples/TestProviderWithUseExisting/test.spec.ts @@ -1,6 +1,10 @@ import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, MockInstance, MockReset } from 'ng-mocks'; +import { + MockBuilder, + MockInstance, + MockRender, + MockReset, +} from 'ng-mocks'; // A service we want to use. @Injectable() @@ -53,7 +57,9 @@ describe('TestProviderWithUseExisting', () => { afterAll(MockReset); it('creates TargetService', () => { - const service = TestBed.get(TargetService); + const service = MockRender< + TargetService & Partial<{ name: string }> + >(TargetService).point.componentInstance; // Because Service2 has been replaced with a mock copy, // we are getting here a mock copy of Service2 instead of Service1. diff --git a/examples/TestProviderWithUseFactory/test.spec.ts b/examples/TestProviderWithUseFactory/test.spec.ts index 25546a2bc9..f4697f4353 100644 --- a/examples/TestProviderWithUseFactory/test.spec.ts +++ b/examples/TestProviderWithUseFactory/test.spec.ts @@ -1,6 +1,5 @@ import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, MockInstance } from 'ng-mocks'; +import { MockBuilder, MockInstance, MockRender } from 'ng-mocks'; // Dependency 1. @Injectable() @@ -44,7 +43,7 @@ describe('TestProviderWithUseFactory', () => { }); it('creates TargetService', () => { - const service = TestBed.get(TargetService); + const service = MockRender(TargetService).point.componentInstance; // Because Service1 has been replaced with a mock copy, we should get mock1 here. expect(service.service.name).toEqual('mock1'); diff --git a/examples/TestProviderWithUseValue/test.spec.ts b/examples/TestProviderWithUseValue/test.spec.ts index ea671e641b..ef25829632 100644 --- a/examples/TestProviderWithUseValue/test.spec.ts +++ b/examples/TestProviderWithUseValue/test.spec.ts @@ -1,6 +1,5 @@ import { Injectable, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder } from 'ng-mocks'; +import { MockBuilder, MockRender } from 'ng-mocks'; // A simple service, it might have contained more logic, // but it is redundant for the test demonstration. @@ -31,10 +30,11 @@ describe('TestProviderWithUseValue', () => { beforeEach(() => MockBuilder(TargetService, TargetModule)); it('creates TargetService', () => { - const service = TestBed.get(TargetService); + const service = MockRender(TargetService).point + .componentInstance; // Let's assert received data. - expect(service).toEqual({ + expect(service as any).toEqual({ service: null, }); }); diff --git a/examples/TestRoute/test.spec.ts b/examples/TestRoute/test.spec.ts index 98d70a7152..7b39f8eb08 100644 --- a/examples/TestRoute/test.spec.ts +++ b/examples/TestRoute/test.spec.ts @@ -1,6 +1,6 @@ import { Location } from '@angular/common'; import { Component, NgModule } from '@angular/core'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, tick } from '@angular/core/testing'; import { Router, RouterModule, RouterOutlet } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; @@ -66,8 +66,8 @@ describe('TestRoute:Route', () => { it('renders /1 with Target1Component', fakeAsync(() => { const fixture = MockRender(RouterOutlet); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); // First we need to initialize navigation. location.go('/1'); @@ -83,8 +83,8 @@ describe('TestRoute:Route', () => { it('renders /2 with Target2Component', fakeAsync(() => { const fixture = MockRender(RouterOutlet); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); // First we need to initialize navigation. location.go('/2'); @@ -117,8 +117,8 @@ describe('TestRoute:Component', () => { it('navigates between pages', fakeAsync(() => { const fixture = MockRender(TargetComponent); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); // First we need to initialize navigation. if (fixture.ngZone) { diff --git a/examples/TestRoutingGuard/test.spec.ts b/examples/TestRoutingGuard/test.spec.ts index e5b667534b..e7ae7a29a5 100644 --- a/examples/TestRoutingGuard/test.spec.ts +++ b/examples/TestRoutingGuard/test.spec.ts @@ -5,7 +5,7 @@ import { NgModule, VERSION, } from '@angular/core'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, tick } from '@angular/core/testing'; import { CanActivate, Router, @@ -129,8 +129,8 @@ describe('TestRoutingGuard', () => { } const fixture = MockRender(RouterOutlet); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); // First we need to initialize navigation. if (fixture.ngZone) { @@ -146,9 +146,11 @@ describe('TestRoutingGuard', () => { it('loads dashboard', fakeAsync(() => { const fixture = MockRender(RouterOutlet); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); - const loginService: LoginService = TestBed.get(LoginService); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); + const loginService: LoginService = fixture.point.injector.get( + LoginService, + ); // Letting the guard know we have been logged in. loginService.isLoggedIn = true; diff --git a/examples/TestRoutingResolver/test.spec.ts b/examples/TestRoutingResolver/test.spec.ts index 545f2b54de..4fd896d4e8 100644 --- a/examples/TestRoutingResolver/test.spec.ts +++ b/examples/TestRoutingResolver/test.spec.ts @@ -1,6 +1,6 @@ import { Location } from '@angular/common'; import { Component, Injectable, NgModule } from '@angular/core'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, tick } from '@angular/core/testing'; import { ActivatedRoute, Resolve, @@ -91,9 +91,11 @@ describe('TestRoutingResolver', () => { // It is important to run routing tests in fakeAsync. it('provides data to on the route', fakeAsync(() => { const fixture = MockRender(RouterOutlet); - const router: Router = TestBed.get(Router); - const location: Location = TestBed.get(Location); - const dataService: DataService = TestBed.get(DataService); + const router: Router = fixture.point.injector.get(Router); + const location: Location = fixture.point.injector.get(Location); + const dataService: DataService = fixture.point.injector.get( + DataService, + ); // DataService has been replaced with a mock copy, // let's set a custom value we will assert later on. diff --git a/examples/TestToken/test.spec.ts b/examples/TestToken/test.spec.ts index 6cc62a1921..54b518a7a7 100644 --- a/examples/TestToken/test.spec.ts +++ b/examples/TestToken/test.spec.ts @@ -1,6 +1,5 @@ import { Injectable, InjectionToken, NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { MockBuilder, ngMocks } from 'ng-mocks'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; const TOKEN_CLASS = new InjectionToken('CLASS'); const TOKEN_EXISTING = new InjectionToken('EXISTING'); @@ -57,7 +56,8 @@ describe('TestToken', () => { }); it('creates TOKEN_CLASS', () => { - const token = TestBed.get(TOKEN_CLASS); + const token = MockRender(TOKEN_CLASS).point + .componentInstance; // Verifying that the token is an instance of ServiceClass. expect(token).toEqual(jasmine.any(ServiceClass)); @@ -65,7 +65,8 @@ describe('TestToken', () => { }); it('creates TOKEN_EXISTING', () => { - const token = TestBed.get(TOKEN_EXISTING); + const token = MockRender(TOKEN_EXISTING).point + .componentInstance; // Verifying that the token is an instance of ServiceExisting. // But because it has been replaced with a mock copy, @@ -75,14 +76,14 @@ describe('TestToken', () => { }); it('creates TOKEN_FACTORY', () => { - const token = TestBed.get(TOKEN_FACTORY); + const token = MockRender(TOKEN_FACTORY).point.componentInstance; // Checking that we have here what factory has been created. expect(token).toEqual('FACTORY'); }); it('creates TOKEN_VALUE', () => { - const token = TestBed.get(TOKEN_VALUE); + const token = MockRender(TOKEN_VALUE).point.componentInstance; // Checking the set value. expect(token).toEqual('VALUE'); diff --git a/lib/common/core.helpers.ts b/lib/common/core.helpers.ts index e327abf2ff..7f8c847d4a 100644 --- a/lib/common/core.helpers.ts +++ b/lib/common/core.helpers.ts @@ -2,7 +2,7 @@ import { InjectionToken } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; import coreReflectJit from './core.reflect.jit'; -import { Type } from './core.types'; +import { AnyType, Type } from './core.types'; export const getTestBedInjection = (token: Type | InjectionToken): I | undefined => { const testBed: any = getTestBed(); @@ -79,22 +79,22 @@ export const extractDependency = (deps: any[], set?: Set): void => { } }; -const extendClassicClass = (base: Type): Type => { +const extendClassicClass = (base: AnyType): Type => { let child: any; + // First we try to eval es2015 style and if it fails to use es5 transpilation in the catch block. + // The next step is to respect constructor parameters as the parent class via jitReflector. (window as any).ngMocksParent = base; // istanbul ignore next try { // tslint:disable-next-line no-eval eval(` - class MockMiddleware extends window.ngMocksParent { - } - window.ngMocksResult = MockMiddleware - `); + class MockMiddleware extends window.ngMocksParent {} + window.ngMocksResult = MockMiddleware + `); child = (window as any).ngMocksResult; } catch (e) { class MockMiddleware extends (window as any).ngMocksParent {} - child = MockMiddleware; } (window as any).ngMocksParent = undefined; @@ -102,18 +102,20 @@ const extendClassicClass = (base: Type): Type => { return child; }; -// First we try to eval es2015 style and if it fails to use es5 transpilation in the catch block. -// The next step is to respect constructor parameters as the parent class via jitReflector. -export const extendClass = (base: Type): Type => { - const child: typeof base & - Partial<{ - parameters: any[][]; - }> = extendClassicClass(base); +export const extendClass = (base: AnyType): Type => { + const child: Type = extendClassicClass(base); + Object.defineProperty(child, 'name', { + configurable: true, + value: `MockMiddleware${base.name}`, + writable: true, + }); const parameters = coreReflectJit().parameters(base); if (parameters.length) { Object.defineProperty(child, 'parameters', { + configurable: true, value: [...parameters], + writable: true, }); } diff --git a/lib/common/core.reflect.body.ts b/lib/common/core.reflect.body-catch.ts similarity index 100% rename from lib/common/core.reflect.body.ts rename to lib/common/core.reflect.body-catch.ts diff --git a/lib/common/core.reflect.body-global.ts b/lib/common/core.reflect.body-global.ts new file mode 100644 index 0000000000..f2ea216352 --- /dev/null +++ b/lib/common/core.reflect.body-global.ts @@ -0,0 +1,11 @@ +import coreReflectJit from './core.reflect.jit'; +import { Type } from './core.types'; +import ngMocksUniverse from './ng-mocks-universe'; + +export default (type: Type) => (): T => { + if (!ngMocksUniverse.global.has(type)) { + ngMocksUniverse.global.set(type, new type(coreReflectJit())); + } + + return ngMocksUniverse.global.get(type); +}; diff --git a/lib/common/core.reflect.directive-resolve.ts b/lib/common/core.reflect.directive-resolve.ts index 7507dc2fc7..e70a8d54b0 100644 --- a/lib/common/core.reflect.directive-resolve.ts +++ b/lib/common/core.reflect.directive-resolve.ts @@ -1,6 +1,6 @@ import { Directive } from '@angular/core'; -import coreReflectBody from './core.reflect.body'; +import coreReflectBodyCatch from './core.reflect.body-catch'; import coreReflectDirective from './core.reflect.directive'; -export default (def: any): Directive => coreReflectBody((arg: any) => coreReflectDirective().resolve(arg))(def); +export default (def: any): Directive => coreReflectBodyCatch((arg: any) => coreReflectDirective().resolve(arg))(def); diff --git a/lib/common/core.reflect.directive.ts b/lib/common/core.reflect.directive.ts index af65f5c7c7..319881d9ec 100644 --- a/lib/common/core.reflect.directive.ts +++ b/lib/common/core.reflect.directive.ts @@ -1,12 +1,5 @@ import { MockDirectiveResolver } from '@angular/compiler/testing'; -import coreReflectJit from './core.reflect.jit'; -import ngMocksUniverse from './ng-mocks-universe'; +import coreReflectBodyGlobal from './core.reflect.body-global'; -export default (): MockDirectiveResolver => { - if (!ngMocksUniverse.global.has(MockDirectiveResolver)) { - ngMocksUniverse.global.set(MockDirectiveResolver, new MockDirectiveResolver(coreReflectJit())); - } - - return ngMocksUniverse.global.get(MockDirectiveResolver); -}; +export default coreReflectBodyGlobal(MockDirectiveResolver); diff --git a/lib/common/core.reflect.module-resolve.ts b/lib/common/core.reflect.module-resolve.ts index b6bba0b53b..093eedc9f2 100644 --- a/lib/common/core.reflect.module-resolve.ts +++ b/lib/common/core.reflect.module-resolve.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; -import coreReflectBody from './core.reflect.body'; +import coreReflectBodyCatch from './core.reflect.body-catch'; import coreReflectModule from './core.reflect.module'; -export default (def: any): NgModule => coreReflectBody((arg: any) => coreReflectModule().resolve(arg))(def); +export default (def: any): NgModule => coreReflectBodyCatch((arg: any) => coreReflectModule().resolve(arg))(def); diff --git a/lib/common/core.reflect.module.ts b/lib/common/core.reflect.module.ts index 09441df15e..d44de65614 100644 --- a/lib/common/core.reflect.module.ts +++ b/lib/common/core.reflect.module.ts @@ -1,12 +1,5 @@ import { MockNgModuleResolver } from '@angular/compiler/testing'; -import coreReflectJit from './core.reflect.jit'; -import ngMocksUniverse from './ng-mocks-universe'; +import coreReflectBodyGlobal from './core.reflect.body-global'; -export default (): MockNgModuleResolver => { - if (!ngMocksUniverse.global.has(MockNgModuleResolver)) { - ngMocksUniverse.global.set(MockNgModuleResolver, new MockNgModuleResolver(coreReflectJit())); - } - - return ngMocksUniverse.global.get(MockNgModuleResolver); -}; +export default coreReflectBodyGlobal(MockNgModuleResolver); diff --git a/lib/common/core.reflect.pipe-resolve.ts b/lib/common/core.reflect.pipe-resolve.ts index 65c5020e7d..91d831a171 100644 --- a/lib/common/core.reflect.pipe-resolve.ts +++ b/lib/common/core.reflect.pipe-resolve.ts @@ -1,6 +1,6 @@ import { Pipe } from '@angular/core'; -import coreReflectBody from './core.reflect.body'; +import coreReflectBodyCatch from './core.reflect.body-catch'; import coreReflectPipe from './core.reflect.pipe'; -export default (def: any): Pipe => coreReflectBody((arg: any) => coreReflectPipe().resolve(arg))(def); +export default (def: any): Pipe => coreReflectBodyCatch((arg: any) => coreReflectPipe().resolve(arg))(def); diff --git a/lib/common/core.reflect.pipe.ts b/lib/common/core.reflect.pipe.ts index c793e18061..87c071792f 100644 --- a/lib/common/core.reflect.pipe.ts +++ b/lib/common/core.reflect.pipe.ts @@ -1,12 +1,5 @@ import { MockPipeResolver } from '@angular/compiler/testing'; -import coreReflectJit from './core.reflect.jit'; -import ngMocksUniverse from './ng-mocks-universe'; +import coreReflectBodyGlobal from './core.reflect.body-global'; -export default (): MockPipeResolver => { - if (!ngMocksUniverse.global.has(MockPipeResolver)) { - ngMocksUniverse.global.set(MockPipeResolver, new MockPipeResolver(coreReflectJit())); - } - - return ngMocksUniverse.global.get(MockPipeResolver); -}; +export default coreReflectBodyGlobal(MockPipeResolver); diff --git a/lib/common/core.tokens.ts b/lib/common/core.tokens.ts index 450a93c8d7..da3ae89bea 100644 --- a/lib/common/core.tokens.ts +++ b/lib/common/core.tokens.ts @@ -1,13 +1,11 @@ import { InjectionToken } from '@angular/core'; import { MetadataOverride } from '@angular/core/testing'; -import { AbstractType, Type } from './core.types'; +import { AnyType } from './core.types'; export const NG_MOCKS = new InjectionToken>('NG_MOCKS'); export const NG_MOCKS_TOUCHES = new InjectionToken>('NG_MOCKS_TOUCHES'); -export const NG_MOCKS_OVERRIDES = new InjectionToken | AbstractType, MetadataOverride>>( - 'NG_MOCKS_OVERRIDES', -); +export const NG_MOCKS_OVERRIDES = new InjectionToken, MetadataOverride>>('NG_MOCKS_OVERRIDES'); export const NG_MOCKS_GUARDS = new InjectionToken('NG_MOCKS_GUARDS'); export const NG_MOCKS_INTERCEPTORS = new InjectionToken('NG_MOCKS_INTERCEPTORS'); export const NG_MOCKS_ROOT_PROVIDERS = new InjectionToken('NG_MOCKS_ROOT_PROVIDERS'); diff --git a/lib/common/decorate.mock.ts b/lib/common/decorate.mock.ts index 3362ac460a..b2e3f80fa5 100644 --- a/lib/common/decorate.mock.ts +++ b/lib/common/decorate.mock.ts @@ -1,11 +1,11 @@ import { AnyType } from './core.types'; import { ngMocksMockConfig } from './mock'; -export default function (base: AnyType, mockClass: AnyType, config: ngMocksMockConfig = {}): void { - Object.defineProperties(base, { - mockOf: { value: mockClass }, - name: { value: `MockOf${mockClass.name}` }, - nameConstructor: { value: base.name }, +export default function (mock: AnyType, source: AnyType, config: ngMocksMockConfig = {}): void { + Object.defineProperties(mock, { + mockOf: { value: source }, + name: { value: `MockOf${source.name}` }, + nameConstructor: { value: mock.name }, }); - base.prototype.__ngMocksConfig = config; + mock.prototype.__ngMocksConfig = config; } diff --git a/lib/common/func.is-ng-def.ts b/lib/common/func.is-ng-def.ts index 12ec4c8232..37fd4ee083 100644 --- a/lib/common/func.is-ng-def.ts +++ b/lib/common/func.is-ng-def.ts @@ -1,6 +1,7 @@ -import { PipeTransform } from '@angular/core'; +import { InjectionToken, PipeTransform } from '@angular/core'; import { Type } from './core.types'; +import { isNgInjectionToken } from './func.is-ng-injection-token'; import { isNgType } from './func.is-ng-type'; const isModuleCheck = (def: any, ngType?: string): boolean => (!ngType || ngType === 'm') && isNgType(def, 'NgModule'); @@ -47,6 +48,13 @@ export function isNgDef(declaration: any, ngType: 'p'): declaration is Type; +/** + * Checks whether a class was decorated by @Injectable. + * + * @see https://github.com/ike18t/ng-mocks#isngdef + */ +export function isNgDef(declaration: any, ngType: 't'): declaration is InjectionToken; + /** * Checks whether a class was decorated by a ng type. * @@ -55,6 +63,10 @@ export function isNgDef(declaration: any, ngType: 'i'): declaration is Type export function isNgDef(declaration: any): declaration is Type; export function isNgDef(declaration: any, ngType?: string): declaration is Type { + if (ngType === 't') { + return isNgInjectionToken(declaration); + } + const isModule = isModuleCheck(declaration, ngType); const isComponent = isComponentCheck(declaration, ngType); const isDirective = isDirectiveCheck(declaration, ngType); diff --git a/lib/common/func.is-ng-type.ts b/lib/common/func.is-ng-type.ts index d82b759027..986bb336d6 100644 --- a/lib/common/func.is-ng-type.ts +++ b/lib/common/func.is-ng-type.ts @@ -1,7 +1,7 @@ import coreReflectJit from './core.reflect.jit'; -import { Type } from './core.types'; +import { AnyType } from './core.types'; -export const isNgType = (declaration: Type, type: string): boolean => +export const isNgType = (declaration: AnyType, type: string): boolean => coreReflectJit() .annotations(declaration) .some(annotation => annotation.ngMetadataName === type); diff --git a/lib/common/mock-control-value-accessor.ts b/lib/common/mock-control-value-accessor.ts index f0b2520c25..4c941370e6 100644 --- a/lib/common/mock-control-value-accessor.ts +++ b/lib/common/mock-control-value-accessor.ts @@ -42,12 +42,12 @@ export class LegacyControlValueAccessor extends Mock { */ export interface MockControlValueAccessor { /** - * @see https://github.com/ike18t/ng-mocks#how-to-create-a-mock-form-control + * @see https://github.com/ike18t/ng-mocks#how-to-mock-form-controls */ __simulateChange(value: any): void; /** - * @see https://github.com/ike18t/ng-mocks#how-to-create-a-mock-form-control + * @see https://github.com/ike18t/ng-mocks#how-to-mock-form-controls */ __simulateTouch(): void; } @@ -57,7 +57,7 @@ export interface MockControlValueAccessor { */ export interface MockValidator { /** - * @see https://github.com/ike18t/ng-mocks#how-to-create-a-mock-form-control + * @see https://github.com/ike18t/ng-mocks#how-to-mock-form-controls */ __simulateValidatorChange(): void; } diff --git a/lib/mock-component/mock-component.ts b/lib/mock-component/mock-component.ts index b2af90a142..79beac80da 100644 --- a/lib/mock-component/mock-component.ts +++ b/lib/mock-component/mock-component.ts @@ -138,7 +138,7 @@ export function MockComponents(...components: Array>): Array(component: Type): Type> { // We are inside of an 'it'. It's fine to to return a mock copy. diff --git a/lib/mock-declaration/mock-declaration.spec.ts b/lib/mock-declaration/mock-declaration.spec.ts index 3de11c015e..978c8da29d 100644 --- a/lib/mock-declaration/mock-declaration.spec.ts +++ b/lib/mock-declaration/mock-declaration.spec.ts @@ -44,24 +44,38 @@ describe('MockDeclaration', () => { TargetPipe, ); expect(mocks.length).toEqual(3); - expect(mocks[0].nameConstructor).toEqual('MockMiddleware'); - expect(mocks[1].nameConstructor).toEqual('MockMiddleware'); - expect(mocks[2].nameConstructor).toEqual('MockMiddleware'); + expect(mocks[0].nameConstructor).toEqual( + 'MockMiddlewareComponentMockBase', + ); + expect(mocks[1].nameConstructor).toEqual( + 'MockMiddlewareDirectiveMockBase', + ); + expect(mocks[2].nameConstructor).toEqual('MockMiddlewareMock'); + expect(mocks[0].name).toEqual('MockOfTargetComponent'); + expect(mocks[1].name).toEqual('MockOfTargetDirective'); + expect(mocks[2].name).toEqual('MockOfTargetPipe'); }); it('should process components with an empty template correctly', () => { const mock: any = MockDeclaration(TargetComponent); - expect(mock.nameConstructor).toEqual('MockMiddleware'); + expect(mock.nameConstructor).toEqual( + 'MockMiddlewareComponentMockBase', + ); + expect(mock.name).toEqual('MockOfTargetComponent'); }); it('should process directives correctly', () => { const mock: any = MockDeclaration(TargetDirective); - expect(mock.nameConstructor).toEqual('MockMiddleware'); + expect(mock.nameConstructor).toEqual( + 'MockMiddlewareDirectiveMockBase', + ); + expect(mock.name).toEqual('MockOfTargetDirective'); }); it('should process pipes correctly', () => { const mock: any = MockDeclaration(TargetPipe); - expect(mock.nameConstructor).toEqual('MockMiddleware'); + expect(mock.nameConstructor).toEqual('MockMiddlewareMock'); + expect(mock.name).toEqual('MockOfTargetPipe'); }); it('should skip unknown types', () => { diff --git a/lib/mock-directive/mock-directive.ts b/lib/mock-directive/mock-directive.ts index 4bb38b34b0..9940b290ed 100644 --- a/lib/mock-directive/mock-directive.ts +++ b/lib/mock-directive/mock-directive.ts @@ -73,7 +73,7 @@ export function MockDirectives(...directives: Array>): Array(directive: Type): Type> { // We are inside of an 'it'. diff --git a/lib/mock-module/mock-module.ts b/lib/mock-module/mock-module.ts index 79efdf96e3..24a7c00f99 100644 --- a/lib/mock-module/mock-module.ts +++ b/lib/mock-module/mock-module.ts @@ -145,12 +145,12 @@ const generateReturn = ( : mockModule; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-module + * @see https://github.com/ike18t/ng-mocks#how-to-mock-modules */ export function MockModule(module: Type): Type; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-module + * @see https://github.com/ike18t/ng-mocks#how-to-mock-modules */ export function MockModule(module: NgModuleWithProviders): NgModuleWithProviders; diff --git a/lib/mock-pipe/mock-pipe.ts b/lib/mock-pipe/mock-pipe.ts index cc6d1d5c74..6ff5ca0991 100644 --- a/lib/mock-pipe/mock-pipe.ts +++ b/lib/mock-pipe/mock-pipe.ts @@ -13,7 +13,7 @@ import helperMockService from '../mock-service/helper.mock-service'; import { MockedPipe } from './types'; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-pipe + * @see https://github.com/ike18t/ng-mocks#how-to-mock-pipes */ export function MockPipes(...pipes: Array>): Array> { return pipes.map(pipe => MockPipe(pipe, undefined)); @@ -37,7 +37,7 @@ const getMockClass = (pipe: Type, transform?: PipeTransform['transform']): }; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-pipe + * @see https://github.com/ike18t/ng-mocks#how-to-mock-pipes */ export function MockPipe( pipe: Type, diff --git a/lib/mock-provider/mock-provider.ts b/lib/mock-provider/mock-provider.ts index 2bb6372f72..d01517319d 100644 --- a/lib/mock-provider/mock-provider.ts +++ b/lib/mock-provider/mock-provider.ts @@ -12,17 +12,17 @@ export function MockProviders(...providers: Array | InjectionToken< } /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-provider + * @see https://github.com/ike18t/ng-mocks#how-to-mock-providers */ export function MockProvider(instance: AnyType, overrides?: Partial): FactoryProvider; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-provider + * @see https://github.com/ike18t/ng-mocks#how-to-mock-providers */ export function MockProvider(provider: InjectionToken | string, useValue?: Partial): FactoryProvider; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-provider + * @see https://github.com/ike18t/ng-mocks#how-to-mock-providers */ export function MockProvider(provider: string, useValue?: Partial): FactoryProvider; diff --git a/lib/mock-render/func.generate-template.ts b/lib/mock-render/func.generate-template.ts new file mode 100644 index 0000000000..119e36c6f9 --- /dev/null +++ b/lib/mock-render/func.generate-template.ts @@ -0,0 +1,52 @@ +import { EventEmitter } from '@angular/core'; +import { Subject } from 'rxjs'; + +const solveOutput = (output: any): string => { + if (typeof output === 'function') { + return '($event)'; + } + if (output && typeof output === 'object' && output instanceof EventEmitter) { + return '.emit($event)'; + } + if (output && typeof output === 'object' && output instanceof Subject) { + return '.next($event)'; + } + + return '=$event'; +}; + +const generateTemplateAttrWrap = (prop: string, type: 'i' | 'o') => (type === 'i' ? `[${prop}]` : `(${prop})`); + +const generateTemplateAttrWithParams = (params: any, prop: string, type: 'i' | 'o'): string => + ` ${generateTemplateAttrWrap(prop, type)}="${prop}${type === 'o' ? solveOutput(params[prop]) : ''}"`; + +const generateTemplateAttrWithoutParams = (key: string, value: string, type: 'i' | 'o'): string => + ` ${generateTemplateAttrWrap(key, type)}="${value}${type === 'o' ? '.emit($event)' : ''}"`; + +const generateTemplateAttr = (params: any, attr: any, type: 'i' | 'o') => { + let mockTemplate = ''; + for (const definition of attr) { + const [property, alias] = definition.split(': '); + mockTemplate += params + ? generateTemplateAttrWithParams(params, alias || property, type) + : generateTemplateAttrWithoutParams(alias || property, property, type); + } + + return mockTemplate; +}; + +export default (declaration: any, { selector, params, inputs, outputs }: any): string => { + let mockTemplate = ''; + + // istanbul ignore else + if (typeof declaration === 'string') { + mockTemplate = declaration; + } else if (selector) { + mockTemplate += `<${selector}`; + mockTemplate += generateTemplateAttr(params, inputs, 'i'); + mockTemplate += generateTemplateAttr(params, outputs, 'o'); + mockTemplate += `>`; + } + + return mockTemplate; +}; diff --git a/lib/mock-render/func.install-prop-reader.ts b/lib/mock-render/func.install-prop-reader.ts new file mode 100644 index 0000000000..bdf7b66829 --- /dev/null +++ b/lib/mock-render/func.install-prop-reader.ts @@ -0,0 +1,37 @@ +import helperMockService from '../mock-service/helper.mock-service'; + +const createProperty = (pointComponentInstance: Record, key: string) => { + return { + configurable: true, + get: () => { + if (typeof pointComponentInstance[key] === 'function') { + return (...args: any[]) => pointComponentInstance[key](...args); + } + + return pointComponentInstance[key]; + }, + set: (v: any) => (pointComponentInstance[key] = v), + }; +}; + +const extractAllKeys = (instance: object) => [ + ...helperMockService.extractPropertiesFromPrototype(Object.getPrototypeOf(instance)), + ...helperMockService.extractMethodsFromPrototype(Object.getPrototypeOf(instance)), + ...Object.keys(instance), +]; + +const extractOwnKeys = (instance: object) => [...Object.getOwnPropertyNames(instance), ...Object.keys(instance)]; + +export default (reader: Record, source?: Record, force: boolean = false): void => { + if (!source) { + return; + } + const exists = extractOwnKeys(reader); + for (const key of extractAllKeys(source)) { + if (!force && exists.indexOf(key) !== -1) { + continue; + } + Object.defineProperty(reader, key, createProperty(source, key)); + exists.push(key); + } +}; diff --git a/lib/mock-render/mock-render.spec.ts b/lib/mock-render/mock-render.spec.ts index 5bf0ab254f..6eaecd81ee 100644 --- a/lib/mock-render/mock-render.spec.ts +++ b/lib/mock-render/mock-render.spec.ts @@ -170,9 +170,16 @@ describe('MockRender', () => { expect(mock.getElementById).toHaveBeenCalledWith('test'); }); - it('does not render a component without selector', () => { + it('does render a component without selector', () => { const fixture = MockRender(WithoutSelectorComponent); - expect(fixture.nativeElement.innerHTML).toEqual(''); + expect(fixture.nativeElement.innerHTML).toContain( + 'WithoutSelectorComponent', + ); + }); + + it('renders empty templates w/o point', () => { + const fixture = MockRender(''); + expect(fixture.point).toBeUndefined(); }); it('assigns outputs to a literals', () => { diff --git a/lib/mock-render/mock-render.ts b/lib/mock-render/mock-render.ts index 869605690f..b6cfb440e1 100644 --- a/lib/mock-render/mock-render.ts +++ b/lib/mock-render/mock-render.ts @@ -1,103 +1,17 @@ -import { Component, EventEmitter } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { Subject } from 'rxjs'; +import { Component, DebugElement, Directive, EventEmitter, InjectionToken } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { extendClass } from '../common/core.helpers'; import coreReflectDirectiveResolve from '../common/core.reflect.directive-resolve'; -import { Type } from '../common/core.types'; +import { AnyType, Type } from '../common/core.types'; +import { isNgDef } from '../common/func.is-ng-def'; import { ngMocks } from '../mock-helper/mock-helper'; -import helperMockService from '../mock-service/helper.mock-service'; +import { MockService } from '../mock-service/mock-service'; +import funcGenerateTemplate from './func.generate-template'; +import funcInstallPropReader from './func.install-prop-reader'; import { IMockRenderOptions, MockedComponentFixture } from './types'; -const solveOutput = (output: any): string => { - if (typeof output === 'function') { - return '($event)'; - } - if (output && typeof output === 'object' && output instanceof EventEmitter) { - return '.emit($event)'; - } - if (output && typeof output === 'object' && output instanceof Subject) { - return '.next($event)'; - } - - return '=$event'; -}; - -const createProperty = (pointComponentInstance: Record, key: string) => { - return { - get: () => { - if (typeof pointComponentInstance[key] === 'function') { - return (...args: any[]) => pointComponentInstance[key](...args); - } - - return pointComponentInstance[key]; - }, - set: (v: any) => (pointComponentInstance[key] = v), - }; -}; - -const extractAllKeys = (instance: object) => [ - ...helperMockService.extractPropertiesFromPrototype(Object.getPrototypeOf(instance)), - ...helperMockService.extractMethodsFromPrototype(Object.getPrototypeOf(instance)), - ...Object.keys(instance), -]; - -const extractOwnKeys = (instance: object) => [...Object.getOwnPropertyNames(instance), ...Object.keys(instance)]; - -const installProxy = ( - componentInstance: Record, - pointComponentInstance?: Record, -): void => { - if (!pointComponentInstance) { - return; - } - - const exists = extractOwnKeys(componentInstance); - for (const key of extractAllKeys(pointComponentInstance)) { - if (exists.indexOf(key) !== -1) { - continue; - } - - Object.defineProperty(componentInstance, key, createProperty(pointComponentInstance, key)); - exists.push(key); - } -}; - -const generateTemplateAttrWrap = (prop: string, type: 'i' | 'o') => (type === 'i' ? `[${prop}]` : `(${prop})`); - -const generateTemplateAttrWithParams = (params: any, prop: string, type: 'i' | 'o'): string => - ` ${generateTemplateAttrWrap(prop, type)}="${prop}${type === 'o' ? solveOutput(params[prop]) : ''}"`; - -const generateTemplateAttrWithoutParams = (key: string, value: string, type: 'i' | 'o'): string => - ` ${generateTemplateAttrWrap(key, type)}="${value}${type === 'o' ? '.emit($event)' : ''}"`; - -const generateTemplateAttr = (params: any, attr: any, type: 'i' | 'o') => { - let mockTemplate = ''; - for (const definition of attr) { - const [property, alias] = definition.split(': '); - mockTemplate += params - ? generateTemplateAttrWithParams(params, alias || property, type) - : generateTemplateAttrWithoutParams(alias || property, property, type); - } - - return mockTemplate; -}; - -const generateTemplate = (declaration: any, { selector, params, inputs, outputs }: any): string => { - let mockTemplate = ''; - - if (typeof declaration === 'string') { - mockTemplate = declaration; - } else if (selector) { - mockTemplate += `<${selector}`; - mockTemplate += generateTemplateAttr(params, inputs, 'i'); - mockTemplate += generateTemplateAttr(params, outputs, 'o'); - mockTemplate += `>`; - } - - return mockTemplate; -}; - const applyParamsToFixtureInstanceGetData = (params: any, keys: string[]) => (!params && keys ? keys : []); const applyParamsToFixtureInstance = ( @@ -106,7 +20,7 @@ const applyParamsToFixtureInstance = ( inputs: string[], outputs: string[], ): void => { - installProxy(instance, params); + funcInstallPropReader(instance, params); for (const definition of applyParamsToFixtureInstanceGetData(params, inputs)) { const [property] = definition.split(': '); instance[property] = undefined; @@ -117,6 +31,37 @@ const applyParamsToFixtureInstance = ( } }; +const registerTemplateMiddleware = (template: AnyType, meta: Directive): void => { + const child = extendClass(template); + if (isNgDef(template, 'c')) { + Component(meta)(child); + } else { + Directive(meta)(child); + } + TestBed.configureTestingModule({ + declarations: [child], + }); +}; + +const reflectTemplate = (template: AnyType): Directive => { + if (!isNgDef(template, 'c') && !isNgDef(template, 'd')) { + return {}; + } + + const meta = { ...coreReflectDirectiveResolve(template) }; + + if (meta.selector && meta.selector.match(/[\[\],]/)) { + meta.selector = ''; + } + + if (!meta.selector) { + meta.selector = `ng-mocks-${Date.now()}-${Math.round(Math.random() * 1000)}`; + registerTemplateMiddleware(template, meta); + } + + return meta; +}; + const generateFixture = ({ params, options, inputs, outputs }: any) => { class MockRenderComponent { public constructor() { @@ -125,7 +70,6 @@ const generateFixture = ({ params, options, inputs, outputs }: any) => { } Component(options)(MockRenderComponent); - ngMocks.flushTestBed(); TestBed.configureTestingModule({ declarations: [MockRenderComponent], }); @@ -133,11 +77,49 @@ const generateFixture = ({ params, options, inputs, outputs }: any) => { return TestBed.createComponent(MockRenderComponent); }; +const fixtureFactory = (template: any, meta: Directive, params: any, flags: any): ComponentFixture => { + const mockTemplate = funcGenerateTemplate(template, { ...meta, params }); + const options: Component = { providers: flags.providers, selector: 'mock-render', template: mockTemplate }; + const fixture: any = generateFixture({ ...meta, params, options }); + if (flags.detectChanges) { + fixture.detectChanges(); + } + + return fixture; +}; + +const isExpectedRender = (template: any): boolean => + typeof template === 'string' || isNgDef(template, 'c') || isNgDef(template, 'd'); + +const renderDeclaration = (fixture: any, template: any, params: any): void => { + fixture.point = fixture.debugElement.children[0] || fixture.debugElement.childNodes[0]; + if (isNgDef(template, 'd')) { + Object.defineProperty(fixture.point, 'componentInstance', { + configurable: true, + get: () => ngMocks.get(fixture.point, template), + }); + } + tryWhen(!params, () => funcInstallPropReader(fixture.componentInstance, fixture.point?.componentInstance)); +}; + +const renderInjection = (fixture: any, template: any, params: any): void => { + const instance = TestBed.get(template); + if (params) { + ngMocks.stub(instance, params); + } + fixture.point = MockService(DebugElement, { + childNodes: [], + children: [], + componentInstance: instance, + nativeElement: MockService(HTMLElement), + }); + funcInstallPropReader(fixture.componentInstance, fixture.point.componentInstance, true); +}; + const tryWhen = (flag: boolean, callback: () => void) => { if (!flag) { return; } - try { // ivy throws Error: Expecting instance of DOM Element callback(); @@ -146,6 +128,15 @@ const tryWhen = (flag: boolean, callback: () => void) => { } }; +/** + * @see https://github.com/ike18t/ng-mocks#mockrender + */ +function MockRender( + template: InjectionToken, + params?: undefined, + detectChanges?: boolean | IMockRenderOptions, +): MockedComponentFixture; + /** * @see https://github.com/ike18t/ng-mocks#mockrender */ @@ -180,6 +171,13 @@ function MockRender(template: Type): MockedComponentFixture; +/** + * An empty string doesn't have point. + * + * @see https://github.com/ike18t/ng-mocks#mockrender + */ +function MockRender(template: ''): ComponentFixture & { point: undefined }; + /** * Without params we shouldn't autocomplete any keys of any types. * @@ -206,27 +204,20 @@ function MockRender = Reco ): MockedComponentFixture; function MockRender>( - template: string | Type, + template: string | Type | InjectionToken, params?: TComponent, flags: boolean | IMockRenderOptions = true, -): MockedComponentFixture { +): any { const flagsObject: IMockRenderOptions = typeof flags === 'boolean' ? { detectChanges: flags } : flags; + const meta: Directive = typeof template === 'string' || isNgDef(template, 't') ? {} : reflectTemplate(template); - let inputs: string[] | undefined; - let outputs: string[] | undefined; - let selector: string | undefined; - if (typeof template !== 'string') { - ({ inputs, outputs, selector } = coreReflectDirectiveResolve(template)); - } - - const mockTemplate = generateTemplate(template, { selector, params, inputs, outputs }); - const options: Component = { providers: flagsObject.providers, selector: 'mock-render', template: mockTemplate }; - const fixture: any = generateFixture({ params, options, inputs, outputs }); - if (flagsObject.detectChanges) { - fixture.detectChanges(); + ngMocks.flushTestBed(); + const fixture: any = fixtureFactory(template, meta, params, flagsObject); + if (isExpectedRender(template)) { + renderDeclaration(fixture, template, params); + } else { + renderInjection(fixture, template, params); } - fixture.point = fixture.debugElement.children[0] || fixture.debugElement.childNodes[0]; - tryWhen(!params, () => installProxy(fixture.componentInstance, fixture.point?.componentInstance)); return fixture; } diff --git a/lib/mock-service/mock-service.ts b/lib/mock-service/mock-service.ts index 37c6c51ab0..19eb6befa4 100644 --- a/lib/mock-service/mock-service.ts +++ b/lib/mock-service/mock-service.ts @@ -44,22 +44,22 @@ const mockVariable = (service: any, prefix: string, callback: typeof MockService }; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + * @see https://github.com/ike18t/ng-mocks#how-to-mock-services */ export function MockService(service: boolean | number | string | null | undefined): undefined; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + * @see https://github.com/ike18t/ng-mocks#how-to-mock-services */ export function MockService(service: AnyType, overrides?: Partial, mockNamePrefix?: string): T; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + * @see https://github.com/ike18t/ng-mocks#how-to-mock-services */ export function MockService(service: AnyType, mockNamePrefix?: string): T; /** - * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + * @see https://github.com/ike18t/ng-mocks#how-to-mock-services */ export function MockService(service: object, mockNamePrefix?: string): T; diff --git a/tests-failures/mock-render-token.ts b/tests-failures/mock-render-token.ts new file mode 100644 index 0000000000..1cc0690c8c --- /dev/null +++ b/tests-failures/mock-render-token.ts @@ -0,0 +1,22 @@ +import { InjectionToken } from '@angular/core'; +import { MockRender } from 'ng-mocks'; + +const tokenObj = new InjectionToken<{ value: string }>('OBJ'); +const tokenBoolean = new InjectionToken('BOOLEAN'); + +// @ts-expect-error: does not accept params +MockRender(tokenObj, {}); + +const fixture1 = MockRender(tokenObj); +// @ts-expect-error: fails due to the void type. +fixture1.componentInstance.value = ''; +// works with the correct type +fixture1.point.componentInstance.value = ''; +// @ts-expect-error: fails due to the wrong type. +fixture1.point.componentInstance.value = 0; + +// works due to the right value. +const fixture2 = MockRender(tokenBoolean); +fixture2.point.componentInstance = true; +// @ts-expect-error: fails due to the wrong type. +fixture2.point.componentInstance = ''; diff --git a/tests/issue-266/test.spec.ts b/tests/issue-266/test.spec.ts new file mode 100644 index 0000000000..1c50b3dfb2 --- /dev/null +++ b/tests/issue-266/test.spec.ts @@ -0,0 +1,117 @@ +import { + Component, + Directive, + Injectable, + Input, + NgModule, + Pipe, + PipeTransform, +} from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +@Component({ + template: 'target', +}) +class TargetComponent { + public readonly name = 'component'; +} + +@Directive({ + selector: '[target],[target1]', +}) +class TargetDirective { + public readonly name = 'directive'; + @Input() public readonly value = ''; +} + +@Pipe({ + name: 'target', +}) +class TargetPipe implements PipeTransform { + public readonly value = ''; + + public transform(value: string): number { + return this.value.length + value.length; + } +} + +@Injectable() +class TargetService { + public readonly name = 'target'; + public readonly value: string = ''; +} + +@NgModule({ + declarations: [TargetComponent, TargetDirective, TargetPipe], + exports: [TargetComponent, TargetDirective, TargetPipe], + providers: [TargetService], +}) +class TargetModule {} + +describe('issue-248', () => { + beforeEach(() => MockBuilder(TargetModule)); + + it('renders components w/o selectors', () => { + const fixture = MockRender(TargetComponent); + + expect(fixture.point).toBeDefined(); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(TargetComponent), + ); + expect(fixture.point.componentInstance.name).toEqual('component'); + expect(fixture.point.nativeElement.innerHTML).toEqual('target'); + }); + + it('renders directives', () => { + const params = { + value: '123', + }; + const fixture = MockRender(TargetDirective, params); + + expect(fixture.point).toBeDefined(); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(TargetDirective), + ); + expect(fixture.point.componentInstance.name).toEqual('directive'); + expect(fixture.point.componentInstance.value).toEqual('123'); + + // DetectChanges doesn't break the pointer. + params.value = '321'; + fixture.detectChanges(); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(TargetDirective), + ); + expect(fixture.point.componentInstance.value).toEqual('321'); + }); + + it('fails on not provided pipes', () => { + expect(() => MockRender(TargetPipe)).toThrow(); + }); + + it('renders services', () => { + const fixture = MockRender(TargetService); + + expect(fixture.point).toBeDefined(); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(TargetService), + ); + expect(fixture.point.componentInstance.name).toEqual('target'); + expect(fixture.point.componentInstance.value).toEqual(''); + }); + + it('renders services with params', () => { + const params = { + value: '123', + }; + const fixture = MockRender(TargetService, params); + + expect(fixture.point).toBeDefined(); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(TargetService), + ); + expect(fixture.point.componentInstance.value).toEqual('123'); + + fixture.componentInstance.value = '321'; + expect(fixture.point.componentInstance.value).toEqual('321'); + }); +}); diff --git a/tests/mock-render-tokens/test.spec.ts b/tests/mock-render-tokens/test.spec.ts new file mode 100644 index 0000000000..f54bf50bd7 --- /dev/null +++ b/tests/mock-render-tokens/test.spec.ts @@ -0,0 +1,33 @@ +import { InjectionToken, NgModule } from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +const token = new InjectionToken<{ value: string }>('TOKEN'); +const tokenFail = new InjectionToken<{ value: string }>('TOKEN1'); + +@NgModule({ + providers: [ + { + provide: token, + useValue: { + value: 'target', + }, + }, + ], +}) +class TargetModule {} + +describe('mock-render-tokens', () => { + beforeEach(() => MockBuilder(TargetModule)); + + it('renders tokens', () => { + const fixture = MockRender(token); + expect(fixture.point.componentInstance).toEqual( + jasmine.any(Object), + ); + expect(fixture.point.componentInstance.value).toEqual('target'); + }); + + it('fails on unprovided tokens', () => { + expect(() => MockRender(tokenFail)).toThrow(); + }); +});