diff --git a/e2e/a12/jest.es2015ivy.js b/e2e/a12/jest.es2015ivy.js index fffff1c670..60ac273202 100644 --- a/e2e/a12/jest.es2015ivy.js +++ b/e2e/a12/jest.es2015ivy.js @@ -1,6 +1,5 @@ module.exports = { preset: 'jest-preset-angular', - workerIdleMemoryLimit: '1024MB', maxWorkers: 1, setupFilesAfterEnv: ['/src/setup-jest.ts'], testURL: 'http://localhost', diff --git a/e2e/a12/jest.es5ivy.js b/e2e/a12/jest.es5ivy.js index 58f483a513..f27314943b 100644 --- a/e2e/a12/jest.es5ivy.js +++ b/e2e/a12/jest.es5ivy.js @@ -1,6 +1,5 @@ module.exports = { preset: 'jest-preset-angular', - workerIdleMemoryLimit: '1024MB', maxWorkers: 1, setupFilesAfterEnv: ['/src/setup-jest.ts'], testURL: 'http://localhost', diff --git a/e2e/a12/package.json b/e2e/a12/package.json index ec17a8e044..316bce34d8 100644 --- a/e2e/a12/package.json +++ b/e2e/a12/package.json @@ -17,7 +17,7 @@ "test:jest": "npm run test:jest:es5:ivy && npm run test:jest:es2015:ivy", "test:jest:es5:ivy": "jest --config jest.es5ivy.js", "test:jest:es2015:ivy": "jest --config jest.es2015ivy.js", - "test:jest:debug": "jest -i --watch" + "test:jest:debug": "npm run test:jest:es2015:ivy -- -i --watch" }, "dependencies": { "@angular/animations": "12.2.17", diff --git a/e2e/a12/src/setup-jest.ts b/e2e/a12/src/setup-jest.ts index 97068c45ab..643adca72c 100644 --- a/e2e/a12/src/setup-jest.ts +++ b/e2e/a12/src/setup-jest.ts @@ -1,4 +1,4 @@ -import 'jest-preset-angular'; +import 'jest-preset-angular/setup-jest'; import { ngMocks } from 'ng-mocks'; ngMocks.autoSpy('jest'); diff --git a/libs/ng-mocks/src/lib/mock-component/mock-component.spec.ts b/libs/ng-mocks/src/lib/mock-component/mock-component.spec.ts index 896dcba331..aba96dc068 100644 --- a/libs/ng-mocks/src/lib/mock-component/mock-component.spec.ts +++ b/libs/ng-mocks/src/lib/mock-component/mock-component.spec.ts @@ -475,11 +475,30 @@ describe('MockComponent', () => { }), ], + __vcrIf_key_i1: [ + jasmine.objectContaining({ + selector: 'ngIf_key_i1', + isViewQuery: true, + static: false, + read: ViewContainerRef, + ngMetadataName: 'ViewChild', + }), + ], + __trIf_key_i1: [ + jasmine.objectContaining({ + selector: 'ngIf_key_i1', + isViewQuery: true, + static: false, + read: TemplateRef, + ngMetadataName: 'ViewChild', + }), + ], __mockView_key_i1: [ jasmine.objectContaining({ selector: 'key_i1', isViewQuery: true, static: false, + read: ViewContainerRef, ngMetadataName: 'ViewChild', }), ], @@ -490,20 +509,58 @@ describe('MockComponent', () => { ngMetadataName: 'ContentChild', }), ], + __vcrIf_prop_o1: [ + jasmine.objectContaining({ + selector: 'ngIf_prop_o1', + isViewQuery: true, + static: false, + read: ViewContainerRef, + ngMetadataName: 'ViewChild', + }), + ], + __trIf_prop_o1: [ + jasmine.objectContaining({ + selector: 'ngIf_prop_o1', + isViewQuery: true, + static: false, + read: TemplateRef, + ngMetadataName: 'ViewChild', + }), + ], __mockView_prop_o1: [ jasmine.objectContaining({ selector: 'prop_o1', isViewQuery: true, static: false, + read: ViewContainerRef, ngMetadataName: 'ViewChild', }), ], + __vcrIf_key_i2: [ + jasmine.objectContaining({ + selector: 'ngIf_key_i2', + isViewQuery: true, + static: false, + read: ViewContainerRef, + ngMetadataName: 'ViewChild', + }), + ], + __trIf_key_i2: [ + jasmine.objectContaining({ + selector: 'ngIf_key_i2', + isViewQuery: true, + static: false, + read: TemplateRef, + ngMetadataName: 'ViewChild', + }), + ], __mockView_key_i2: [ jasmine.objectContaining({ selector: 'key_i2', isViewQuery: true, static: false, + read: ViewContainerRef, ngMetadataName: 'ViewChild', }), ], @@ -514,11 +571,30 @@ describe('MockComponent', () => { ngMetadataName: 'ContentChildren', }), ], + __vcrIf_prop_o2: [ + jasmine.objectContaining({ + selector: 'ngIf_prop_o2', + isViewQuery: true, + static: false, + read: ViewContainerRef, + ngMetadataName: 'ViewChild', + }), + ], + __trIf_prop_o2: [ + jasmine.objectContaining({ + selector: 'ngIf_prop_o2', + isViewQuery: true, + static: false, + read: TemplateRef, + ngMetadataName: 'ViewChild', + }), + ], __mockView_prop_o2: [ jasmine.objectContaining({ selector: 'prop_o2', isViewQuery: true, static: false, + read: ViewContainerRef, ngMetadataName: 'ViewChild', }), ], diff --git a/libs/ng-mocks/src/lib/mock-component/mock-component.ts b/libs/ng-mocks/src/lib/mock-component/mock-component.ts index 0bd3b51580..4fae1e1701 100644 --- a/libs/ng-mocks/src/lib/mock-component/mock-component.ts +++ b/libs/ng-mocks/src/lib/mock-component/mock-component.ts @@ -1,5 +1,5 @@ import { - AfterContentInit, + AfterViewInit, ChangeDetectorRef, Component, EmbeddedViewRef, @@ -31,8 +31,11 @@ const mixRenderPrepareVcr = ( selector: string, cdr: ChangeDetectorRef, ): ViewContainerRef | undefined => { - if (!instance[`ngMocksRender_${type}_${selector}`]) { - instance[`ngMocksRender_${type}_${selector}`] = true; + const vcrNgIf: ViewContainerRef = instance[`__vcrIf_${type}_${selector}`]; + const trNgIf: TemplateRef = instance[`__trIf_${type}_${selector}`]; + + if (vcrNgIf && trNgIf && !instance[`ngMocksRender_${type}_${selector}`]) { + instance[`ngMocksRender_${type}_${selector}`] = vcrNgIf.createEmbeddedView(trNgIf, {}); cdr.detectChanges(); } @@ -152,13 +155,14 @@ const mixHide = (instance: MockConfig & Record, changeDetector: mixHideHandler(instance, type, selector, indices); if (!indices) { - instance[`ngMocksRender_${type}_${selector}`] = false; + (instance[`ngMocksRender_${type}_${selector}`] as EmbeddedViewRef).destroy(); + instance[`ngMocksRender_${type}_${selector}`] = undefined; } changeDetector.detectChanges(); }); }; -class ComponentMockBase extends LegacyControlValueAccessor implements AfterContentInit { +class ComponentMockBase extends LegacyControlValueAccessor implements AfterViewInit { // istanbul ignore next public constructor( injector: Injector, @@ -172,7 +176,7 @@ class ComponentMockBase extends LegacyControlValueAccessor implements AfterConte } } - public ngAfterContentInit(): void { + public ngAfterViewInit(): void { const config = (this.__ngMocksConfig as any).config; if (!(this as any).__rendered && config && config.render) { for (const block of Object.keys(config.render)) { diff --git a/libs/ng-mocks/src/lib/mock-component/render/generate-template.ts b/libs/ng-mocks/src/lib/mock-component/render/generate-template.ts index da227d836e..3b5dbe6202 100644 --- a/libs/ng-mocks/src/lib/mock-component/render/generate-template.ts +++ b/libs/ng-mocks/src/lib/mock-component/render/generate-template.ts @@ -1,9 +1,13 @@ import { Query, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; -const viewChildArgs: any = { read: ViewContainerRef, static: false }; +const vcrArgs: any = { read: ViewContainerRef, static: false }; +const trArgs: any = { read: TemplateRef, static: false }; -const viewChildTemplate = (selector: string, key: string): string => - `
`; +const viewChildTemplate = (selector: string, key: string): string => { + const content = `
`; + + return `${content}`; +}; const isTemplateRefQuery = (query: Query): boolean => { if (query.isViewQuery) { @@ -28,16 +32,23 @@ export default (queries?: Record): string => { for (const key of Object.keys(queries)) { const query: Query = queries[key]; + if (key.indexOf('__mock') === 0) { + continue; + } if (!isTemplateRefQuery(query)) { continue; } if (typeof query.selector === 'string') { const selector = query.selector.replace(new RegExp('\\W', 'mg'), '_'); - queries[`__mockView_key_${selector}`] = new ViewChild(`key_${selector}`, viewChildArgs); + queries[`__vcrIf_key_${selector}`] = new ViewChild(`ngIf_key_${selector}`, vcrArgs); + queries[`__trIf_key_${selector}`] = new ViewChild(`ngIf_key_${selector}`, trArgs); + queries[`__mockView_key_${selector}`] = new ViewChild(`key_${selector}`, vcrArgs); queries[`__mockTpl_key_${selector}`] = query; parts.push(viewChildTemplate(selector, 'key')); } - queries[`__mockView_prop_${key}`] = new ViewChild(`prop_${key}`, viewChildArgs); + queries[`__vcrIf_prop_${key}`] = new ViewChild(`ngIf_prop_${key}`, vcrArgs); + queries[`__trIf_prop_${key}`] = new ViewChild(`ngIf_prop_${key}`, trArgs); + queries[`__mockView_prop_${key}`] = new ViewChild(`prop_${key}`, vcrArgs); parts.push(viewChildTemplate(key, 'prop')); } diff --git a/tests/issue-8884/test.spec.ts b/tests/issue-8884/test.spec.ts new file mode 100644 index 0000000000..7bb8a6192c --- /dev/null +++ b/tests/issue-8884/test.spec.ts @@ -0,0 +1,125 @@ +import { NgTemplateOutlet } from '@angular/common'; +import { + Component, + ContentChild, + NgModule, + TemplateRef, + VERSION, +} from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// @see https://github.com/help-me-mom/ng-mocks/issues/8884 +// New control flow doesn't import NgIf or CommonModule by default. +// However, it still allows to use conditions, and if `ContentChild` is used, +// it causes errors such as "Can't bind to 'ngIf' since it isn't a known property of 'div'". +// The fix is to remove dependency on NgIf or CommonModule. +describe('issue-8884', () => { + ngMocks.throwOnConsole(); + + if (Number.parseInt(VERSION.major, 10) < 17) { + it('needs a17+', () => { + expect(true).toBeTruthy(); + }); + + return; + } + + describe('standalone component without NgIf', () => { + @Component({ + selector: 'standalone-8884', + ['standalone' as never]: true, + ['imports' as never]: [NgTemplateOutlet], + template: ``, + }) + class Standalone8884Component { + @ContentChild('content', {} as never) + content?: TemplateRef; + } + + describe('real', () => { + beforeEach(() => MockBuilder(Standalone8884Component)); + + it('renders content', () => { + const fixture = MockRender(` + + content + + `); + + expect(ngMocks.formatText(fixture)).toEqual('content'); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(null, Standalone8884Component)); + + it('renders content', () => { + const fixture = MockRender(` + + content + + `); + expect(ngMocks.formatText(fixture)).toEqual(''); + + ngMocks.render( + ngMocks.findInstance(Standalone8884Component), + ngMocks.findTemplateRef('content'), + ); + expect(ngMocks.formatText(fixture)).toEqual('content'); + }); + }); + }); + + describe('classic component without NgIf import in its module', () => { + @Component({ + selector: 'target-8884', + template: ``, + }) + class Target8884Component { + @ContentChild('content', {} as never) + content?: TemplateRef; + } + + @NgModule({ + imports: [NgTemplateOutlet], + declarations: [Target8884Component], + exports: [Target8884Component], + }) + class TargetModule {} + + describe('real', () => { + beforeEach(() => MockBuilder(TargetModule)); + + it('renders content', () => { + const fixture = MockRender(` + + content + + `); + + expect(ngMocks.formatText(fixture)).toEqual('content'); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(null, TargetModule)); + + it('render contents', () => { + const fixture = MockRender(` + + content + + `); + + expect(ngMocks.formatText(fixture)).toEqual(''); + + ngMocks.render( + ngMocks.findInstance(Target8884Component), + ngMocks.findTemplateRef('content'), + ); + expect(ngMocks.formatText(fixture)).toEqual('content'); + }); + }); + }); +});