Skip to content

Commit

Permalink
Merge pull request #575 from satanTime/issues/572
Browse files Browse the repository at this point in the history
fix(core): a config parameter to suppress MockRender errors #572
  • Loading branch information
satanTime authored May 20, 2021
2 parents caa2f9f + bcfe23a commit 933b579
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 53 deletions.
15 changes: 15 additions & 0 deletions docs/articles/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ The only breaking change is `auto-spy`.
[`ngMocks.autoSpy('jasmine')`](./extra/auto-spy.md) and [`ngMocks.autoSpy('jest')`](./extra/auto-spy.md)
should be used instead of `import 'ng-mocks/dist/jasmine';` and `import 'ng-mocks/dist/jest';`.

## From 11.10 to 11.11 and higher

If you are facing an issue with `MockRender` and a thrown error about "Forgot to flush TestBed?",
you may want to suppress it instead of fixing.

In order to suppress the error you need to upgrade to `12.0.1` at least, and in `test.ts` to add:

```ts
ngMocks.config({
onTestBedFlushNeed: 'warn',
});
```

Then instead of throwing errors, `MockRender` will log them in console as warnings.

## From 10 to 11

#### MockModule
Expand Down
30 changes: 29 additions & 1 deletion e2e/a12/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ngMocksUniverse from '../common/ng-mocks-universe';

import mockHelperCrawl from './crawl/mock-helper.crawl';
import mockHelperReveal from './crawl/mock-helper.reveal';
import mockHelperRevealAll from './crawl/mock-helper.reveal-all';
Expand Down Expand Up @@ -38,6 +40,13 @@ export default {
autoSpy: mockHelperAutoSpy,
change: mockHelperChange,
click: mockHelperClick,
config: (config: { onTestBedFlushNeed?: 'throw' | 'warn' | 'default' }) => {
if (config.onTestBedFlushNeed === 'warn') {
ngMocksUniverse.global.set('warnOnTestBedFlushNeed', true);
} else if (config.onTestBedFlushNeed === 'throw' || config.onTestBedFlushNeed === 'default') {
ngMocksUniverse.global.delete('warnOnTestBedFlushNeed');
}
},
crawl: mockHelperCrawl,
defaultMock: mockHelperDefaultMock,
event: mockHelperEvent,
Expand Down
15 changes: 12 additions & 3 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.throw-on-console.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// tslint:disable no-console

import coreDefineProperty from '../common/core.define-property';

const factory =
(propName: string) =>
(...args: any[]) => {
const error = new Error(args.join(' '));
coreDefineProperty(error, 'ngMocksConsoleCatch', propName, false);
throw error;
};

// Thanks Ivy, it does not throw an error and we have to use injector.
export default (): void => {
let backupWarn: typeof console.warn;
Expand All @@ -9,9 +19,8 @@ export default (): void => {
backupWarn = console.warn;
backupError = console.error;
// istanbul ignore next
console.error = console.warn = (...args: any[]) => {
throw new Error(args.join(' '));
};
console.warn = factory('warn');
console.error = factory('error');
});

afterAll(() => {
Expand Down
2 changes: 2 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const ngMocks: {
*/
click(elSelector: HTMLElement | DebugNodeSelector, payload?: Partial<MouseEvent>): void;

config(config: { onTestBedFlushNeed?: 'throw' | 'warn' | 'default' }): void;

/**
* @see https://ng-mocks.sudo.eu/api/ngMocks/crawl
*/
Expand Down
43 changes: 43 additions & 0 deletions libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Component, Directive } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { Type } from '../common/core.types';
import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor';

import funcGenerateTemplate from './func.generate-template';
import funcInstallPropReader from './func.install-prop-reader';

const generateWrapper = ({ params, options, inputs }: any) => {
class MockRenderComponent {
public constructor() {
if (!params) {
for (const input of inputs || []) {
let value: any = null;
helperDefinePropertyDescriptor(this, input, {
get: () => value,
set: (newValue: any) => (value = newValue),
});
}
}
funcInstallPropReader(this, params);
}
}

Component(options)(MockRenderComponent);
TestBed.configureTestingModule({
declarations: [MockRenderComponent],
});

return MockRenderComponent;
};

export default (template: any, meta: Directive, params: any, flags: any): Type<any> => {
const mockTemplate = funcGenerateTemplate(template, { ...meta, params });
const options: Component = {
providers: flags.providers,
selector: 'mock-render',
template: mockTemplate,
};

return generateWrapper({ ...meta, params, options });
};
75 changes: 26 additions & 49 deletions libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, DebugElement, Directive, InjectionToken } from '@angular/core';
import { DebugElement, Directive, InjectionToken } from '@angular/core';
import { getTestBed, TestBed } from '@angular/core/testing';

import coreDefineProperty from '../common/core.define-property';
Expand All @@ -10,7 +10,7 @@ import { ngMocks } from '../mock-helper/mock-helper';
import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor';
import { MockService } from '../mock-service/mock-service';

import funcGenerateTemplate from './func.generate-template';
import funcCreateWrapper from './func.create-wrapper';
import funcInstallPropReader from './func.install-prop-reader';
import funcReflectTemplate from './func.reflect-template';
import { DefaultRenderComponent, IMockRenderOptions, MockedComponentFixture } from './types';
Expand All @@ -21,41 +21,6 @@ interface MockRenderFactory<C = any, F = DefaultRenderComponent<C>> {
(): MockedComponentFixture<C, F>;
}

const generateWrapper = ({ params, options, inputs }: any) => {
class MockRenderComponent {
public constructor() {
if (!params) {
for (const input of inputs || []) {
let value: any = null;
helperDefinePropertyDescriptor(this, input, {
get: () => value,
set: (newValue: any) => (value = newValue),
});
}
}
funcInstallPropReader(this, params);
}
}

Component(options)(MockRenderComponent);
TestBed.configureTestingModule({
declarations: [MockRenderComponent],
});

return MockRenderComponent;
};

const createWrapper = (template: any, meta: Directive, params: any, flags: any): Type<any> => {
const mockTemplate = funcGenerateTemplate(template, { ...meta, params });
const options: Component = {
providers: flags.providers,
selector: 'mock-render',
template: mockTemplate,
};

return generateWrapper({ ...meta, params, options });
};

const isExpectedRender = (template: any): boolean =>
typeof template === 'string' || isNgDef(template, 'c') || isNgDef(template, 'd');

Expand Down Expand Up @@ -95,18 +60,33 @@ const tryWhen = (flag: boolean, callback: () => void) => {
}
};

const fixtureMessage = [
'Forgot to flush TestBed?',
'MockRender cannot be used without a reset after TestBed.get / TestBed.inject / TestBed.createComponent and another MockRender in the same test.',
'To flush TestBed, add a call of ngMocks.flushTestBed() before the call of MockRender, or pass `reset: true` to MockRender options.',
'If you want to mock a service before rendering, consider usage of MockInstance.',
].join(' ');

const handleFixtureError = (e: any) => {
const message = [
'Forgot to flush TestBed?',
'MockRender cannot be used without a reset after TestBed.get / TestBed.inject / TestBed.createComponent and another MockRender in the same test.',
'To flush TestBed, add a call of ngMocks.flushTestBed() before the call of MockRender, or pass `reset: true` to MockRender options.',
'If you want to mock a service before rendering, consider usage of MockInstance.',
].join(' ');
const error = new Error(message);
const error = new Error(fixtureMessage);
coreDefineProperty(error, 'parent', e, false);
throw error;
};

const flushTestBed = (flags: Record<string, any>): void => {
const testBed: any = getTestBed();
if (flags.reset || (!testBed._instantiated && !testBed._testModuleRef)) {
ngMocks.flushTestBed();
} else if (
ngMocksUniverse.global.get('warnOnTestBedFlushNeed') &&
(testBed._testModuleRef || testBed._instantiated)
) {
// tslint:disable-next-line:no-console
console.warn(fixtureMessage);
ngMocks.flushTestBed();
}
};

const generateFactory = (componentCtor: Type<any>, flags: any, params: any, template: any) => {
const result = () => {
const fixture: any = TestBed.createComponent(componentCtor);
Expand Down Expand Up @@ -224,12 +204,9 @@ function MockRenderFactory<MComponent, TComponent extends Record<keyof any, any>
const flagsObject: IMockRenderOptions = typeof flags === 'boolean' ? { detectChanges: flags } : { ...flags };
const meta: Directive = typeof template === 'string' || isNgDef(template, 't') ? {} : funcReflectTemplate(template);

const testBed: any = getTestBed();
if (flagsObject.reset || (!testBed._instantiated && !testBed._testModuleRef)) {
ngMocks.flushTestBed();
}
flushTestBed(flagsObject);
try {
const componentCtor: any = createWrapper(template, meta, params, flagsObject);
const componentCtor: any = funcCreateWrapper(template, meta, params, flagsObject);

return generateFactory(componentCtor, flagsObject, params, template);
} catch (e) {
Expand Down
64 changes: 64 additions & 0 deletions tests/issue-572/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// tslint:disable no-console

import { Component, Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

@Component({
selector: 'target',
template: 'target',
})
class TargetComponent {}

describe('issue-572', () => {
ngMocks.faster();
let consoleWarn: typeof console.warn;

beforeAll(() => MockBuilder(TargetComponent));
beforeAll(() => (consoleWarn = console.warn));

beforeEach(() => {
console.warn = jasmine.createSpy('console.warn');
});

afterAll(() => {
console.warn = consoleWarn;
ngMocks.config({ onTestBedFlushNeed: 'default' });
});

it('throws on TestBed change', () => {
try {
TestBed.get(Injector);
MockRender(TargetComponent);
fail('should throw');
} catch (e) {
expect(console.warn).not.toHaveBeenCalled();
expect(e).not.toEqual(
jasmine.objectContaining({
ngMocksConsoleCatch: jasmine.anything(),
}),
);
}
});

it('warns via console on TestBed change', () => {
ngMocks.config({ onTestBedFlushNeed: 'warn' });

TestBed.get(Injector);
expect(console.warn).not.toHaveBeenCalled();
const fixture = MockRender(TargetComponent);
expect(console.warn).toHaveBeenCalled();

// renders properly
expect(ngMocks.formatText(fixture)).toEqual('target');
});

it('keeps the config', () => {
ngMocks.config({});

TestBed.get(Injector);
expect(console.warn).not.toHaveBeenCalled();
MockRender(TargetComponent);
expect(console.warn).toHaveBeenCalled();
});
});
35 changes: 35 additions & 0 deletions tests/ng-mocks-throw-on-console/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// tslint:disable no-console

import { ngMocks } from 'ng-mocks';

describe('ng-mocks-throw-on-console', () => {
ngMocks.throwOnConsole();

it('throws on warn', () => {
try {
console.warn('warn message');
fail('should have failed');
} catch (e) {
expect(e).toEqual(
jasmine.objectContaining({
message: 'warn message',
ngMocksConsoleCatch: 'warn',
}),
);
}
});

it('throws on error', () => {
try {
console.error('error message');
fail('should have failed');
} catch (e) {
expect(e).toEqual(
jasmine.objectContaining({
message: 'error message',
ngMocksConsoleCatch: 'error',
}),
);
}
});
});

0 comments on commit 933b579

Please sign in to comment.