From df4418c60382e9ce89defbdd0ffdc1a3728d39a7 Mon Sep 17 00:00:00 2001 From: MG Date: Sat, 8 May 2021 23:20:15 +0200 Subject: [PATCH] feat(faster): supports MockRender in beforeAll #488 --- docs/articles/api/ngMocks/faster.md | 61 +++++++-- .../ng-mocks/src/lib/common/ng-mocks-stack.ts | 99 ++++++++++++++ .../src/lib/mock-helper/mock-helper.faster.ts | 29 ++-- .../src/lib/mock-instance/mock-instance.ts | 86 +++++------- .../src/lib/mock-render/mock-render.ts | 6 +- .../e2e/src/issue-488/faster.spec.ts | 129 ++++++++++++++++++ tests/issue-488/faster.spec.ts | 93 +++++++++++++ 7 files changed, 428 insertions(+), 75 deletions(-) create mode 100644 libs/ng-mocks/src/lib/common/ng-mocks-stack.ts create mode 100644 tests-angular/e2e/src/issue-488/faster.spec.ts create mode 100644 tests/issue-488/faster.spec.ts diff --git a/docs/articles/api/ngMocks/faster.md b/docs/articles/api/ngMocks/faster.md index cb46032a17..32d281a5fb 100644 --- a/docs/articles/api/ngMocks/faster.md +++ b/docs/articles/api/ngMocks/faster.md @@ -7,7 +7,7 @@ There is a `ngMocks.faster` feature that optimizes setup of similar test modules and reduces required time on their execution. Imagine a situation when `beforeEach` creates the same setup used by dozens of `it`. -This is the case where `ngMocks.faster` might be useful, simply call it before `beforeEach` and +This is the case where `ngMocks.faster` might be useful, simply call it before `beforeAll` and **the Angular tests will run faster**. ```ts @@ -15,7 +15,7 @@ describe('performance:correct', () => { ngMocks.faster(); // <-- add it before // The TestBed is not going to be changed between tests. - beforeEach(() => { + beforeAll(() => { return MockBuilder(TargetComponent, TargetModule).keep(TargetService); }); @@ -34,11 +34,10 @@ describe('performance:correct', () => { If a test creates spies in `beforeEach` then this should be tuned, because `ngMocks.faster` will detect this difference and display a notice. -A possible solution is usage of [MockInstance](../MockInstance.md) or to move creation of spies -outside of `beforeEach`. +A possible solution is usage of [MockInstance](../MockInstance.md) instead of manual declaration, +or to move creation of spies outside of `beforeEach`. -
Click to see an example of MockInstance -

+## Example of MockInstance ```ts describe('beforeEach:mock-instance', () => { @@ -66,11 +65,7 @@ describe('beforeEach:mock-instance', () => { }); ``` -

-
- -
Click to see an example of optimizing spies in beforeEach -

+## Example of optimizing spies in beforeEach ```ts describe('beforeEach:manual-spy', () => { @@ -101,5 +96,45 @@ describe('beforeEach:manual-spy', () => { }); ``` -

-
+## MockRender + +Usage of `ngMocks.faster()` covers [`MockRender`](../MockRender.md) too. + +With its help, `MockRender` can be called in either `beforeEach` or `beforeAll`. +`beforeAll` won't reset its fixture after a test, and the fixture can be used in the next test. +Please pay attention that state of components also stays the same. + +```ts +describe('issue-488:faster', () => { + let fixture: MockedComponentFixture; + + ngMocks.faster(); + + beforeAll(() => MockBuilder(MyComponent, MyModule)); + beforeAll(() => fixture = MockRender(MyComponent)); + + it('first test has initial render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + + it('second test continues the prev state', () => { + expect(ngMocks.formatText(fixture)).toEqual('0'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); +}); +``` diff --git a/libs/ng-mocks/src/lib/common/ng-mocks-stack.ts b/libs/ng-mocks/src/lib/common/ng-mocks-stack.ts new file mode 100644 index 0000000000..bb6ddd4841 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/ng-mocks-stack.ts @@ -0,0 +1,99 @@ +import { mapValues } from './core.helpers'; +import ngMocksUniverse from './ng-mocks-universe'; + +export interface NgMocksStack { + id: object; + mockInstance?: any[]; +} + +type NgMocksStackCallback = (state: NgMocksStack, stack: NgMocksStack[]) => void; + +// istanbul ignore next +const stack: NgMocksStack[] = ngMocksUniverse.global.get('reporter-stack') ?? []; +ngMocksUniverse.global.set('reporter-stack', stack); + +// istanbul ignore next +const listenersPush: Set = ngMocksUniverse.global.get('reporter-stack-push') ?? new Set(); +ngMocksUniverse.global.set('reporter-stack-push', listenersPush); + +// istanbul ignore next +const listenersPop: Set = ngMocksUniverse.global.get('reporter-stack-pop') ?? new Set(); +ngMocksUniverse.global.set('reporter-stack-pop', listenersPop); + +const stackPush = () => { + const id = {}; + ngMocksUniverse.global.set('reporter-stack-id', id); + const state = { id }; + stack.push(state); + + for (const callback of mapValues(listenersPush)) { + callback(state, stack); + } +}; +const stackPop = () => { + const state = stack.pop(); + // istanbul ignore if + if (stack.length === 0) { + const id = {}; + stack.push({ id }); + } + + // istanbul ignore else + if (state) { + for (const callback of mapValues(listenersPop)) { + callback(state, stack); + } + } + + ngMocksUniverse.global.set('reporter-stack-id', stack[stack.length - 1].id); +}; + +const reporterStack: jasmine.CustomReporter = { + jasmineDone: stackPop, + jasmineStarted: stackPush, + specDone: stackPop, + specStarted: stackPush, + suiteDone: stackPop, + suiteStarted: stackPush, +}; + +const install = () => { + if (!ngMocksUniverse.global.has('reporter-stack-install')) { + jasmine.getEnv().addReporter(reporterStack); + ngMocksUniverse.global.set('reporter-stack-install', true); + stackPush(); + } + + return ngMocksUniverse.global.has('reporter-stack-install'); +}; + +// istanbul ignore next +const subscribePush = (callback: NgMocksStackCallback) => { + listenersPush.add(callback); + if (stack.length) { + callback(stack[stack.length - 1], stack); + } +}; + +// istanbul ignore next +const subscribePop = (callback: NgMocksStackCallback) => { + listenersPop.add(callback); +}; + +// istanbul ignore next +const unsubscribePush = (callback: NgMocksStackCallback) => { + listenersPush.delete(callback); +}; + +// istanbul ignore next +const unsubscribePop = (callback: NgMocksStackCallback) => { + listenersPop.delete(callback); +}; + +export default { + install, + subscribePop, + subscribePush, + unsubscribePop, + unsubscribePush, +}; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts index 407036c7f1..5bcb87172a 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts @@ -1,25 +1,38 @@ -import { getTestBed, TestBed } from '@angular/core/testing'; +import { ComponentFixture, getTestBed, TestBed } from '@angular/core/testing'; +import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack'; import ngMocksUniverse from '../common/ng-mocks-universe'; import mockHelperFlushTestBed from './mock-helper.flush-test-bed'; +const resetFixtures = (stack: NgMocksStack) => { + const activeFixtures: Array & { ngMocksStackId?: any }> = + (getTestBed() as any)._activeFixtures || /* istanbul ignore next */ []; + + for (let i = activeFixtures.length - 1; i >= 0; i -= 1) { + if (!activeFixtures[i].ngMocksStackId || activeFixtures[i].ngMocksStackId === stack.id) { + activeFixtures[i].destroy(); + activeFixtures.splice(i, 1); + } + } + if (activeFixtures.length === 0) { + mockHelperFlushTestBed(); + } +}; + export default () => { + ngMocksStack.install(); + beforeAll(() => { if (ngMocksUniverse.global.has('bullet:customized')) { TestBed.resetTestingModule(); } ngMocksUniverse.global.set('bullet', true); - }); - - afterEach(() => { - mockHelperFlushTestBed(); - for (const fixture of (getTestBed() as any)._activeFixtures || /* istanbul ignore next */ []) { - fixture.destroy(); - } + ngMocksStack.subscribePop(resetFixtures); }); afterAll(() => { + ngMocksStack.unsubscribePop(resetFixtures); ngMocksUniverse.global.delete('bullet'); if (ngMocksUniverse.global.has('bullet:reset')) { TestBed.resetTestingModule(); diff --git a/libs/ng-mocks/src/lib/mock-instance/mock-instance.ts b/libs/ng-mocks/src/lib/mock-instance/mock-instance.ts index 13981111e8..9aab26ab7e 100644 --- a/libs/ng-mocks/src/lib/mock-instance/mock-instance.ts +++ b/libs/ng-mocks/src/lib/mock-instance/mock-instance.ts @@ -2,60 +2,42 @@ import { InjectionToken, Injector } from '@angular/core'; import { AbstractType, Type } from '../common/core.types'; import funcImportExists from '../common/func.import-exists'; +import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack'; import ngMocksUniverse from '../common/ng-mocks-universe'; -const stack: any[][] = [[]]; -const stackPush = () => { - stack.push([]); -}; -const stackPop = () => { - for (const declaration of stack.pop() || /* istanbul ignore next */ []) { +let currentStack: NgMocksStack; +ngMocksStack.subscribePush(state => { + currentStack = state; +}); +ngMocksStack.subscribePop((state, stack) => { + for (const declaration of state.mockInstance || /* istanbul ignore next */ []) { ngMocksUniverse.configInstance.get(declaration)?.overloads?.pop(); } - // istanbul ignore if - if (stack.length === 0) { - stack.push([]); + currentStack = stack[stack.length - 1]; +}); + +ngMocksStack.subscribePush(() => { + // On start we have to flush any caches, + // they are not from this spec. + const set = ngMocksUniverse.getLocalMocks(); + set.splice(0, set.length); +}); +ngMocksStack.subscribePop(() => { + const set = ngMocksUniverse.getLocalMocks(); + while (set.length) { + const [declaration, config] = set.pop() || /* istanbul ignore next */ []; + const universeConfig = ngMocksUniverse.configInstance.has(declaration) + ? ngMocksUniverse.configInstance.get(declaration) + : {}; + ngMocksUniverse.configInstance.set(declaration, { + ...universeConfig, + ...config, + }); } -}; - -const reporterStack: jasmine.CustomReporter = { - jasmineDone: stackPop, - jasmineStarted: stackPush, - specDone: stackPop, - specStarted: stackPush, - suiteDone: stackPop, - suiteStarted: stackPush, -}; +}); -const reporter: jasmine.CustomReporter = { - specDone: () => { - const set = ngMocksUniverse.getLocalMocks(); - while (set.length) { - const [declaration, config] = set.pop() || /* istanbul ignore next */ []; - const universeConfig = ngMocksUniverse.configInstance.has(declaration) - ? ngMocksUniverse.configInstance.get(declaration) - : {}; - ngMocksUniverse.configInstance.set(declaration, { - ...universeConfig, - ...config, - }); - } - }, - specStarted: () => { - // On start we have to flush any caches, - // they are not from this spec. - const set = ngMocksUniverse.getLocalMocks(); - set.splice(0, set.length); - }, -}; - -let installReporter = true; const restore = (declaration: any, config: any): void => { - if (installReporter) { - jasmine.getEnv().addReporter(reporter); - installReporter = false; - } - + ngMocksStack.install(); ngMocksUniverse.getLocalMocks().push([declaration, config]); }; @@ -101,23 +83,21 @@ const mockInstanceConfig = (declaration: Type | AbstractType | Injectio } }; -let installStackReporter = true; const mockInstanceMember = ( declaration: Type | AbstractType | InjectionToken, name: string, stub: any, encapsulation?: 'get' | 'set', ) => { - if (installStackReporter) { - jasmine.getEnv().addReporter(reporterStack); - installStackReporter = false; - } + ngMocksStack.install(); const config = ngMocksUniverse.configInstance.has(declaration) ? ngMocksUniverse.configInstance.get(declaration) : {}; const overloads = config.overloads || []; overloads.push([name, stub, encapsulation]); config.overloads = overloads; ngMocksUniverse.configInstance.set(declaration, config); - stack[stack.length - 1].push(declaration); + const mockInstances = currentStack.mockInstance ?? []; + mockInstances.push(declaration); + currentStack.mockInstance = mockInstances; return stub; }; diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render.ts b/libs/ng-mocks/src/lib/mock-render/mock-render.ts index 858eb0046a..04f1095635 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render.ts @@ -5,6 +5,7 @@ import coreDefineProperty from '../common/core.define-property'; import { Type } from '../common/core.types'; import funcImportExists from '../common/func.import-exists'; import { isNgDef } from '../common/func.is-ng-def'; +import ngMocksUniverse from '../common/ng-mocks-universe'; import { ngMocks } from '../mock-helper/mock-helper'; import { MockService } from '../mock-service/mock-service'; @@ -25,7 +26,10 @@ const generateFixture = ({ params, options }: any) => { declarations: [MockRenderComponent], }); - return TestBed.createComponent(MockRenderComponent); + const fixture = TestBed.createComponent(MockRenderComponent); + coreDefineProperty(fixture, 'ngMocksStackId', ngMocksUniverse.global.get('reporter-stack-id')); + + return fixture; }; const fixtureFactory = (template: any, meta: Directive, params: any, flags: any): ComponentFixture => { diff --git a/tests-angular/e2e/src/issue-488/faster.spec.ts b/tests-angular/e2e/src/issue-488/faster.spec.ts new file mode 100644 index 0000000000..b7b1509173 --- /dev/null +++ b/tests-angular/e2e/src/issue-488/faster.spec.ts @@ -0,0 +1,129 @@ +import { CommonModule } from '@angular/common'; +import { Component, NgModule, OnInit } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + createAction, + createFeatureSelector, + createReducer, + on, + Store, + StoreFeatureModule, + StoreModule, +} from '@ngrx/store'; +import { + MockBuilder, + MockedComponentFixture, + MockRender, + ngMocks, +} from 'ng-mocks'; + +const increaseValue = createAction('set-value'); +const resetValue = createAction('reset-value'); + +const myReducer = { + featureKey: 'test', + reducer: createReducer( + 0, + on(increaseValue, state => state + 1), + on(resetValue, () => 0), + ), +}; + +const selectValue = createFeatureSelector(myReducer.featureKey); + +@Component({ + selector: 'target', + template: '{{ value$ | async }}', +}) +class MyComponent implements OnInit { + public value$ = this.store.select(selectValue); + + public constructor(private readonly store: Store) {} + + public ngOnInit(): void { + this.store.dispatch(increaseValue()); + } + + public reset(): void { + this.store.dispatch(resetValue()); + } +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [ + CommonModule, + StoreModule.forFeature(myReducer.featureKey, myReducer.reducer), + ], +}) +class MyModule {} + +describe('issue-488', () => { + let fixture: MockedComponentFixture; + + ngMocks.throwOnConsole(); + ngMocks.faster(); + + beforeAll(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot({})) + .keep(StoreFeatureModule), + ); + + describe('faster multi render', () => { + beforeEach(() => (fixture = MockRender(MyComponent))); + + it('first test has brand new render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + TestBed.get(Store).dispatch(increaseValue()); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + + it('second test has brand new render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + TestBed.get(Store).dispatch(increaseValue()); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + }); + + describe('faster single render', () => { + beforeAll(() => (fixture = MockRender(MyComponent))); + + it('first test has render of 1', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + TestBed.get(Store).dispatch(increaseValue()); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + + it('second test continues the prev state', () => { + expect(ngMocks.formatText(fixture)).toEqual('0'); + + TestBed.get(Store).dispatch(increaseValue()); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + }); +}); diff --git a/tests/issue-488/faster.spec.ts b/tests/issue-488/faster.spec.ts new file mode 100644 index 0000000000..b7a816e91e --- /dev/null +++ b/tests/issue-488/faster.spec.ts @@ -0,0 +1,93 @@ +import { Component, NgModule, OnInit } from '@angular/core'; +import { + MockBuilder, + MockedComponentFixture, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Component({ + selector: 'target', + template: '{{ value }}', +}) +class MyComponent implements OnInit { + public value = 0; + + public ngOnInit(): void { + this.value += 1; + } + + public reset(): void { + this.value = 0; + } +} + +@NgModule({ + declarations: [MyComponent], +}) +class MyModule {} + +describe('issue-488:faster', () => { + let fixture: MockedComponentFixture; + + ngMocks.throwOnConsole(); + ngMocks.faster(); + + beforeAll(() => MockBuilder(MyComponent, MyModule)); + + describe('multi render', () => { + beforeEach(() => (fixture = MockRender(MyComponent))); + + it('first test has brand new render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + + it('second test has brand new render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + }); + + describe('single render', () => { + beforeAll(() => (fixture = MockRender(MyComponent))); + + it('first test has initial render', () => { + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('2'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + + it('second test continues the prev state', () => { + expect(ngMocks.formatText(fixture)).toEqual('0'); + + fixture.point.componentInstance.value += 1; + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('1'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('0'); + }); + }); +});