diff --git a/docs/articles/troubleshooting/browser-animations-module.md b/docs/articles/troubleshooting/browser-animations-module.md new file mode 100644 index 0000000000..1659478bd2 --- /dev/null +++ b/docs/articles/troubleshooting/browser-animations-module.md @@ -0,0 +1,41 @@ +--- +title: Testing Modules with BrowserAnimationsModule +description: Information and solutions how to test modules with BrowserAnimationsModule +sidebar_label: BrowserAnimationsModule +--- + +By default, `ng-mocks` replaces `BrowserAnimationsModule` with `NoopAnimationsModule`. + +However, it can be changed via [`MockBuilder`](../api/MockBuilder.md) or [`ngMocks.guts`](../api/ngMocks/guts.md) +when `NoopAnimationsModule` isn't a solution. + +## MockBuilder + +```ts +// No animations at all +MockBuilder(MyComponent, MyModule).exclude(BrowserAnimationsModule); + +// Mock BrowserAnimationsModule +MockBuilder(MyComponent, MyModule).mock(BrowserAnimationsModule); + +// Keep BrowserAnimationsModule to test animations. +MockBuilder(MyComponent, MyModule).keep(BrowserAnimationsModule); +``` + +## ngMocks.guts + +```ts +// No animations at all +ngMocks.guts(MyComponent, MyModule, BrowserAnimationsModule); + +// Mock BrowserAnimationsModule +ngMocks.guts(MyComponent, [MyModule, BrowserAnimationsModule]); + +// Keep BrowserAnimationsModule to test animations. +ngMocks.guts([MyComponent, BrowserAnimationsModule], MyModule); +``` + +## fakeAsync + +A kept / mock `BrowserAnimationsModule` causes issues with `fakeAsync`. +Please open an issue on github, if you have a case where `NoopAnimationsModule` isn't a solution. diff --git a/docs/sidebars.js b/docs/sidebars.js index 29ef32e5c8..40e57bc33e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -104,6 +104,7 @@ module.exports = { 'troubleshooting/no-selector', 'troubleshooting/not-a-known-element', 'troubleshooting/internals-vs-externals', + 'troubleshooting/browser-animations-module', ], }, { diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts index ac5c6e983a..096492f1b5 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts @@ -217,8 +217,18 @@ const generateData = (protoKeep: any, protoMock: any, protoExclude: any): Data = export default (keep: any, mock: any = null, exclude: any = null): TestModuleMetadata => { const data: Data = generateData(keep, mock, exclude); + const resolutions = new Map(); + ngMocksUniverse.config.set('ngMocksDepsResolution', resolutions); + for (const mockDef of mapValues(data.keep)) { + resolutions.set(mockDef, 'keep'); + } + for (const mockDef of mapValues(data.exclude)) { + resolutions.set(mockDef, 'exclude'); + } + ngMocksUniverse.config.set('mockNgDefResolver', new Map()); for (const def of mapValues(data.mock)) { + resolutions.set(def, 'mock'); if (data.optional.has(def)) { continue; } @@ -226,6 +236,7 @@ export default (keep: any, mock: any = null, exclude: any = null): TestModuleMet } const meta = createMeta(data); ngMocksUniverse.config.delete('mockNgDefResolver'); + ngMocksUniverse.config.delete('ngMocksDepsResolution'); return meta; }; diff --git a/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts b/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts index 99aaacd8e2..d51a1e0852 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts @@ -12,6 +12,26 @@ import helperMockService from '../mock-service/helper.mock-service'; import { MockModule } from './mock-module'; +// tslint:disable-next-line variable-name +let BrowserAnimationsModule: any; +// tslint:disable-next-line variable-name +let NoopAnimationsModule: any; +// istanbul ignore next +let replaceWithNoop: (def: any) => boolean = () => false; +try { + // tslint:disable-next-line no-require-imports no-var-requires + const imports = require('@angular/platform-browser/animations'); + BrowserAnimationsModule = imports.BrowserAnimationsModule; + NoopAnimationsModule = imports.NoopAnimationsModule; + replaceWithNoop = (def: any) => + def === BrowserAnimationsModule && + !!BrowserAnimationsModule && + !!NoopAnimationsModule && + !ngMocksUniverse.getResolution(def); +} catch { + // nothing to do +} + const processDefMap: Array<[any, any]> = [ ['c', MockComponent], ['d', MockDirective], @@ -19,6 +39,12 @@ const processDefMap: Array<[any, any]> = [ ]; const processDef = (def: any) => { + // BrowserAnimationsModule is a very special case. + // If it is not resolved manually, we simply replace it with NoopAnimationsModule. + if (replaceWithNoop(def)) { + return NoopAnimationsModule; + } + if (isNgDef(def, 'm') || isNgModuleDefWithProviders(def)) { return MockModule(def as any); } diff --git a/tests/fake-async/test.spec.ts b/tests/fake-async/test.spec.ts new file mode 100644 index 0000000000..00090a7073 --- /dev/null +++ b/tests/fake-async/test.spec.ts @@ -0,0 +1,86 @@ +import { + Component, + Input, + NgZone, + OnDestroy, + OnInit, +} from '@angular/core'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { MockBuilder, MockRenderFactory, ngMocks } from 'ng-mocks'; + +@Component({ + selector: 'target', + template: `{{ counter }}`, +}) +class TargetComponent implements OnInit, OnDestroy { + public counter = 0; + @Input() public value = 0; + private timer: any; + + public constructor(private readonly zone: NgZone) {} + + public ngOnDestroy(): void { + clearInterval(this.timer); + } + + public ngOnInit(): void { + clearInterval(this.timer); + this.zone.runOutsideAngular(() => { + this.timer = setInterval( + () => (this.counter += this.value), + 1000, + ); + }); + } +} + +// The goal is to ensure that nothing is broken with fakeAsync. +describe('fake-async', () => { + let calls = 0; + + const factory = MockRenderFactory(TargetComponent, ['value']); + ngMocks.faster(); + beforeAll(() => MockBuilder(TargetComponent)); + + it('checks with 5', fakeAsync(() => { + const fixture = factory({ value: 1 }); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + expect(ngMocks.formatText(fixture)).toEqual('0'); + tick(1000); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('1'); + tick(2000); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('3'); + + // for an unknown reason the fixture should be destroyed here. + fixture.destroy(); + calls += 1; + }); + })); + + it('checks with 5', fakeAsync(() => { + const fixture = factory({ value: 5 }); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + expect(ngMocks.formatText(fixture)).toEqual('0'); + tick(1000); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('5'); + tick(4000); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual('25'); + + // for an unknown reason the fixture should be destroyed here. + fixture.destroy(); + calls += 1; + }); + })); + + it('controls execution', () => { + expect(calls).toEqual(2); + }); +}); diff --git a/tests/issue-222/dom-shared-styles-host.spec.ts b/tests/issue-222/dom-shared-styles-host.spec.ts index 7cfe6d97f1..f599385bcb 100644 --- a/tests/issue-222/dom-shared-styles-host.spec.ts +++ b/tests/issue-222/dom-shared-styles-host.spec.ts @@ -56,7 +56,11 @@ class TargetComponent { class TargetModule {} describe('issue-222:DomSharedStylesHost:mock', () => { - beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).mock( + BrowserAnimationsModule, + ), + ); it('correctly handles DomSharedStylesHost in a mock module', () => { const fixture = MockRender(TargetComponent); @@ -84,7 +88,10 @@ describe('issue-222:DomSharedStylesHost:keep', () => { describe('issue-222:DomSharedStylesHost:guts', () => { beforeEach(() => TestBed.configureTestingModule( - ngMocks.guts(TargetComponent, TargetModule), + ngMocks.guts(TargetComponent, [ + TargetModule, + BrowserAnimationsModule, + ]), ).compileComponents(), ); diff --git a/tests/issue-641/test.spec.ts b/tests/issue-641/test.spec.ts new file mode 100644 index 0000000000..e8a1a488d3 --- /dev/null +++ b/tests/issue-641/test.spec.ts @@ -0,0 +1,158 @@ +import { + animate, + style, + transition, + trigger, +} from '@angular/animations'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + NgModule, + RendererFactory2, +} from '@angular/core'; +import { fakeAsync, flush, tick } from '@angular/core/testing'; +import { + BrowserAnimationsModule, + NoopAnimationsModule, +} from '@angular/platform-browser/animations'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Component({ + animations: [ + trigger('state', [ + transition('void => *', [ + style({ + backgroundColor: '#000', + color: '#fff', + height: 0, + }), + animate(10000, style({ height: 100 })), + ]), + ]), + ], + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'target', + template: `
+ target +
`, +}) +class TargetComponent { + public show = false; +} + +@NgModule({ + declarations: [TargetComponent], + imports: [CommonModule, BrowserAnimationsModule], +}) +class TargetModule {} + +describe('issue-641', () => { + describe('BrowserAnimationsModule:default', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('works on whenStable and due to NoopAnimationsModule', async () => { + const fixture = MockRender(TargetComponent); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(ngMocks.formatText(fixture)).toEqual('target'); + }); + + it('works on whenStable and due to NoopAnimationsModule', async () => { + const fixture = MockRender(TargetComponent); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(ngMocks.formatText(fixture)).toEqual('target'); + }); + }); + + // unfortunately with real animations it is not so easy. + // tslint:disable-next-line no-disabled-tests + xdescribe('BrowserAnimationsModule:mock', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule) + .mock(BrowserAnimationsModule) + .exclude(RendererFactory2), + ); + + it('fails due to mock BrowserAnimationsModule', fakeAsync(() => { + const fixture = MockRender(TargetComponent); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + tick(10000); + flush(); + + // In a mock BrowserAnimationsModule nothing happens, + // and it doesn't render "target". + expect(ngMocks.formatText(fixture)).toEqual(''); + + fixture.destroy(); + }); + })); + }); + + describe('BrowserAnimationsModule:exclude', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).exclude( + BrowserAnimationsModule, + ), + ); + + it('fails due to missing BrowserAnimationsModule', () => { + expect(() => MockRender(TargetComponent)).toThrowError( + /BrowserAnimationsModule/, + ); + }); + }); + + describe('BrowserAnimationsModule:replace', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).replace( + BrowserAnimationsModule, + NoopAnimationsModule, + ), + ); + + it('works on whenStable and due to NoopAnimationsModule', async () => { + const fixture = MockRender(TargetComponent); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(ngMocks.formatText(fixture)).toEqual('target'); + }); + }); + + // unfortunately with real animations it is not so easy. + // tslint:disable-next-line no-disabled-tests + xdescribe('BrowserAnimationsModule:keep', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).keep( + BrowserAnimationsModule, + ), + ); + + it('works on whenStable and due to BrowserAnimationsModule', fakeAsync(() => { + const fixture = MockRender(TargetComponent); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + // waiting for the animation + tick(9999); + + // we need to wait for real animation ended + expect(ngMocks.formatText(fixture)).not.toEqual('target'); + + // waiting for the animation + tick(1); + + // profit + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.destroy(); + }); + })); + }); +});