Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): replacing animations with noop by default #641 #644

Merged
merged 1 commit into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/articles/troubleshooting/browser-animations-module.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ module.exports = {
'troubleshooting/no-selector',
'troubleshooting/not-a-known-element',
'troubleshooting/internals-vs-externals',
'troubleshooting/browser-animations-module',
],
},
{
Expand Down
11 changes: 11 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,26 @@ 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;
}
resolve(data, def, false);
}
const meta = createMeta(data);
ngMocksUniverse.config.delete('mockNgDefResolver');
ngMocksUniverse.config.delete('ngMocksDepsResolution');

return meta;
};
26 changes: 26 additions & 0 deletions libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,39 @@ 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],
['p', MockPipe],
];

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);
}
Expand Down
86 changes: 86 additions & 0 deletions tests/fake-async/test.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
11 changes: 9 additions & 2 deletions tests/issue-222/dom-shared-styles-host.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
);

Expand Down
158 changes: 158 additions & 0 deletions tests/issue-641/test.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `<div @state (@state.done)="show = true">
<span *ngIf="show">target</span>
</div>`,
})
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();
});
}));
});
});