From b565e5f815dbbb72e2854c9d83e6c4a146a287d5 Mon Sep 17 00:00:00 2001 From: satanTime Date: Wed, 13 Jul 2022 16:27:27 +0200 Subject: [PATCH] feat(MockRender): supports Self providers #3053 --- e2e/a10/package.json | 2 +- e2e/a11/package.json | 2 +- e2e/a9/package.json | 2 +- .../lib/mock-render/func.create-wrapper.ts | 30 +++++- .../lib/mock-render/mock-render-factory.ts | 44 +++++---- package.json | 2 +- tests/issue-3053/test.spec.ts | 96 +++++++++++++++++++ 7 files changed, 152 insertions(+), 26 deletions(-) create mode 100644 tests/issue-3053/test.spec.ts diff --git a/e2e/a10/package.json b/e2e/a10/package.json index db32451542..86112eedf5 100644 --- a/e2e/a10/package.json +++ b/e2e/a10/package.json @@ -16,7 +16,7 @@ "test:jasmine:es2015:ivy": "ng test --ts-config ./tsconfig.es2015ivy.spec.json --progress=false", "test:jasmine:es2015:no-ivy": "ng test --ts-config ./tsconfig.es2015noivy.spec.json --progress=false", "test:jasmine:debug": "ng test -- --watch --browsers Chrome", - "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy &&npm run test:jest:es2015:ivy &&npm run test:jest:es2015:no-ivy", + "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy && npm run test:jest:es2015:ivy && npm run test:jest:es2015:no-ivy", "test:jest:es5:ivy": "jest -i --config jest.es5ivy.js", "test:jest:es5:no-ivy": "jest -i --config jest.es5noivy.js", "test:jest:es2015:ivy": "jest -i --config jest.es2015ivy.js", diff --git a/e2e/a11/package.json b/e2e/a11/package.json index 80f7c6d157..fe00dbe499 100644 --- a/e2e/a11/package.json +++ b/e2e/a11/package.json @@ -16,7 +16,7 @@ "test:jasmine:es2015:ivy": "ng test --ts-config ./tsconfig.es2015ivy.spec.json --progress=false", "test:jasmine:es2015:no-ivy": "ng test --ts-config ./tsconfig.es2015noivy.spec.json --progress=false", "test:jasmine:debug": "ng test -- --watch --browsers Chrome", - "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy &&npm run test:jest:es2015:ivy &&npm run test:jest:es2015:no-ivy", + "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy && npm run test:jest:es2015:ivy && npm run test:jest:es2015:no-ivy", "test:jest:es5:ivy": "jest -i --config jest.es5ivy.js", "test:jest:es5:no-ivy": "jest -i --config jest.es5noivy.js", "test:jest:es2015:ivy": "jest -i --config jest.es2015ivy.js", diff --git a/e2e/a9/package.json b/e2e/a9/package.json index 91e9ada2e2..b05e78a6f2 100644 --- a/e2e/a9/package.json +++ b/e2e/a9/package.json @@ -16,7 +16,7 @@ "test:jasmine:es2015:ivy": "ng test --ts-config ./tsconfig.es2015ivy.spec.json --progress=false", "test:jasmine:es2015:no-ivy": "ng test --ts-config ./tsconfig.es2015noivy.spec.json --progress=false", "test:jasmine:debug": "ng test -- --watch --browsers Chrome", - "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy &&npm run test:jest:es2015:ivy &&npm run test:jest:es2015:no-ivy", + "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es5:no-ivy && npm run test:jest:es2015:ivy && npm run test:jest:es2015:no-ivy", "test:jest:es5:ivy": "jest -i --config jest.es5ivy.js", "test:jest:es5:no-ivy": "jest -i --config jest.es5noivy.js", "test:jest:es2015:ivy": "jest -i --config jest.es2015ivy.js", diff --git a/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts b/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts index 12be436d41..6e1bc29fa1 100644 --- a/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts +++ b/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts @@ -1,8 +1,9 @@ -import { Component, Directive } from '@angular/core'; +import { Component, Directive, Optional, Self } from '@angular/core'; import coreConfig from '../common/core.config'; import coreDefineProperty from '../common/core.define-property'; import { Type } from '../common/core.types'; +import funcGetProvider from '../common/func.get-provider'; import ngMocksUniverse from '../common/ng-mocks-universe'; import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor'; @@ -24,7 +25,7 @@ const generateWrapperOutput = instance[prop] = event; }; -const generateWrapper = ({ bindings, options, inputs }: any) => { +const generateWrapperComponent = ({ bindings, options, inputs }: any) => { class MockRenderComponent { public constructor() { coreDefineProperty(this, '__ngMocksOutput', generateWrapperOutput(this)); @@ -45,6 +46,23 @@ const generateWrapper = ({ bindings, options, inputs }: any) => { return MockRenderComponent; }; +const generateWrapperDirective = ({ selector, options }: any) => { + class MockRenderDirective {} + Directive({ + selector, + providers: options.providers, + })(MockRenderDirective); + + const parameters: any[] = []; + for (const def of options.providers) { + const provider = funcGetProvider(def); + parameters.push([provider, new Optional(), new Self()]); + } + coreDefineProperty(MockRenderDirective, 'parameters', parameters); + + return MockRenderDirective; +}; + const getCache = () => { const caches: Array & Record<'cacheKey', any[]>> = ngMocksUniverse.config.get('MockRenderCaches') ?? []; if (caches.length === 0) { @@ -102,9 +120,15 @@ export default ( viewProviders: flags.viewProviders, }; - ctor = generateWrapper({ ...meta, bindings, options }); + ctor = generateWrapperComponent({ ...meta, bindings, options }); coreDefineProperty(ctor, 'cacheKey', cacheKey); coreDefineProperty(ctor, 'tpl', mockTemplate); + + if (meta.selector && options.providers) { + const dir = generateWrapperDirective({ ...meta, bindings, options }); + coreDefineProperty(ctor, 'providers', dir); + } + caches.unshift(ctor as any); caches.splice(ngMocksUniverse.global.get('mockRenderCacheSize') ?? coreConfig.mockRenderCacheSize); diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts index 60c04eaab6..521c9d3052 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts @@ -110,27 +110,33 @@ const flushTestBed = (flags: Record): void => { } }; -const generateFactoryInstall = (ctor: AnyType, options: IMockRenderFactoryOptions) => () => { - const testBed: TestBed & { - _compiler?: { +const generateFactoryInstall = + (ctor: AnyType & { providers?: AnyType }, options: IMockRenderFactoryOptions) => () => { + const testBed: TestBed & { + _compiler?: { + declarations?: Array>; + }; + _declarations?: Array>; declarations?: Array>; - }; - _declarations?: Array>; - declarations?: Array>; - } = getTestBed(); - // istanbul ignore next - const declarations = testBed._compiler?.declarations || testBed.declarations || testBed._declarations; - if (!declarations || declarations.indexOf(ctor) === -1) { - flushTestBed(options); - try { - TestBed.configureTestingModule({ - declarations: [ctor], - }); - } catch (error) { - handleFixtureError(error); + } = getTestBed(); + // istanbul ignore next + const existing = testBed._compiler?.declarations || testBed.declarations || testBed._declarations; + if (!existing || existing.indexOf(ctor) === -1) { + flushTestBed(options); + try { + const declarations: Array> = []; + if (ctor.providers) { + declarations.push(ctor.providers); + } + declarations.push(ctor); + testBed.configureTestingModule({ + declarations, + }); + } catch (error) { + handleFixtureError(error); + } } - } -}; + }; const generateFactory = ( componentCtor: Type & { tpl?: string }, diff --git a/package.json b/package.json index 1e1e7f411f..49a638d9f3 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "s:test:jest": "node --version", "s:test:min": "node --version", "s:test:nx": "P=e2e/nx/apps/a-nx/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P", - "test:e2e": "npm run test:a5 && npm run test:a6 && npm run test:a7 &&npm run test:a8 && npm run test:a9 && npm run test:a10 && npm run test:a11 && npm run test:a12 && npm run test:a13 && npm run test:a14 && npm run test:jasmine && npm run test:jest && npm run test:min && npm run test:nx", + "test:e2e": "npm run test:a5 && npm run test:a6 && npm run test:a7 && npm run test:a8 && npm run test:a9 && npm run test:a10 && npm run test:a11 && npm run test:a12 && npm run test:a13 && npm run test:a14 && npm run test:jasmine && npm run test:jest && npm run test:min && npm run test:nx", "test:a5": "npm run test:a5es5 && npm run test:a5es2015", "test:a5es5": "cd e2e/a5es5 && npm run test", "test:a5es2015": "cd e2e/a5es2015 && npm run test", diff --git a/tests/issue-3053/test.spec.ts b/tests/issue-3053/test.spec.ts new file mode 100644 index 0000000000..a7117fe197 --- /dev/null +++ b/tests/issue-3053/test.spec.ts @@ -0,0 +1,96 @@ +import { + Component, + Directive, + Injectable, + Self, + VERSION, +} from '@angular/core'; + +import { + MockBuilder, + MockProvider, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Injectable() +class TargetService { + echo() { + return this.constructor.name; + } +} + +@Directive({ + selector: 'target', +}) +class TargetDirective { + constructor(@Self() public service: TargetService) {} +} + +@Component({ + selector: 'target', + template: ``, +}) +class TargetComponent { + constructor(@Self() public service: TargetService) {} +} + +// @see /~https://github.com/help-me-mom/ng-mocks/issues/3053 +// MockRender should create a directive which provides the desired services on @Self level. +describe('issue-3053', () => { + describe('Directive:default', () => { + beforeEach(() => MockBuilder(TargetDirective, TargetService)); + + it('throws because of missing service', () => { + expect(() => MockRender(TargetDirective)).toThrowError( + /No provider for TargetService/, + ); + }); + }); + + describe('Directive:providers', () => { + beforeEach(() => MockBuilder(TargetDirective, TargetService)); + + it('renders with self provider', () => { + expect(() => + MockRender(TargetDirective, null, { + providers: [MockProvider(TargetService)], + }), + ).not.toThrow(); + + const target = ngMocks.findInstance(TargetDirective); + expect(target.service).toBeDefined(); + }); + }); + + if (Number.parseInt(VERSION.major, 10) <= 12) { + // Before Angular 13, directives are injected after components. + // This breaks dependency tree, therefore we should skip those tests. + return; + } + + describe('Component:default', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetService)); + + it('throws because of missing service', () => { + expect(() => MockRender(TargetComponent)).toThrowError( + /No provider for TargetService/, + ); + }); + }); + + describe('Component:providers', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetService)); + + it('renders with self provider', () => { + expect(() => + MockRender(TargetComponent, null, { + providers: [MockProvider(TargetService)], + }), + ).not.toThrow(); + + const target = ngMocks.findInstance(TargetComponent); + expect(target.service).toBeDefined(); + }); + }); +});