diff --git a/libs/ng-mocks/src/lib/mock-helper/crawl/mock-helper.crawl.ts b/libs/ng-mocks/src/lib/mock-helper/crawl/mock-helper.crawl.ts index 3feca7605a..ad71309a6a 100644 --- a/libs/ng-mocks/src/lib/mock-helper/crawl/mock-helper.crawl.ts +++ b/libs/ng-mocks/src/lib/mock-helper/crawl/mock-helper.crawl.ts @@ -8,7 +8,7 @@ import nestedCheck from './nested-check'; export default ( sel: DebugNode | DebugNodeSelector, - callback: (node: DebugNode) => void | boolean, + callback: (node: DebugNode, parent?: DebugNode) => void | boolean, includeTextNode = false, ): void => { const el = mockHelperFind(funcGetLastFixture(), sel, undefined); diff --git a/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check-parent.ts b/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check-parent.ts new file mode 100644 index 0000000000..66fe5f6416 --- /dev/null +++ b/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check-parent.ts @@ -0,0 +1,27 @@ +import { MockedDebugNode } from '../../mock-render/types'; + +import elDefCompare from './el-def-compare'; +import elDefGetNode from './el-def-get-node'; +import elDefGetParent from './el-def-get-parent'; + +const detectParent = (node: MockedDebugNode, parent: MockedDebugNode | undefined): MockedDebugNode | undefined => { + if (parent) { + return parent; + } + + const expected = elDefGetParent(node); + const currentParent = node.parent ? elDefGetNode(node.parent) : undefined; + if (node.parent && elDefCompare(expected, currentParent)) { + return node.parent; + } + for (const childNode of node.parent?.childNodes || []) { + const childElDef = elDefGetNode(childNode); + if (elDefCompare(expected, childElDef)) { + return childNode; + } + } + + return undefined; +}; + +export default ((): typeof detectParent => detectParent)(); diff --git a/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check.ts b/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check.ts index 201fbcb829..0e010fa1a8 100644 --- a/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check.ts +++ b/libs/ng-mocks/src/lib/mock-helper/crawl/nested-check.ts @@ -1,30 +1,8 @@ import { MockedDebugNode } from '../../mock-render/types'; import detectTextNode from './detect-text-node'; -import elDefCompare from './el-def-compare'; -import elDefGetNode from './el-def-get-node'; -import elDefGetParent from './el-def-get-parent'; import nestedCheckChildren from './nested-check-children'; - -const detectParent = (node: MockedDebugNode, parent: MockedDebugNode | undefined): MockedDebugNode | undefined => { - if (parent) { - return parent; - } - - const expected = elDefGetParent(node); - const currentParent = node.parent ? elDefGetNode(node.parent) : undefined; - if (node.parent && elDefCompare(expected, currentParent)) { - return node.parent; - } - for (const childNode of node.parent?.childNodes || []) { - const childElDef = elDefGetNode(childNode); - if (elDefCompare(expected, childElDef)) { - return childNode; - } - } - - return undefined; -}; +import nestedCheckParent from './nested-check-parent'; const nestedCheck = ( node: MockedDebugNode | null | undefined, @@ -38,7 +16,7 @@ const nestedCheck = ( if (!includeTextNode && detectTextNode(node)) { return false; } - if (check(node, detectParent(node, parent))) { + if (check(node, nestedCheckParent(node, parent))) { return true; } diff --git a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts index f9f43bb53e..e4804b4341 100644 --- a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts +++ b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instance.ts @@ -25,8 +25,11 @@ export default (...args: any[]): T => { if (fixture) { mockHelperCrawl( mockHelperFind(fixture, el, undefined), - node => { + (node, parent) => { funcGetFromNode(result, node, declaration); + if (result.length === 0 && parent && parent.nativeNode.nodeName === '#comment') { + funcGetFromNode(result, parent, declaration); + } return result.length > 0; }, diff --git a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts index 5f7b68eb30..df7d740b2c 100644 --- a/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts +++ b/libs/ng-mocks/src/lib/mock-helper/find-instance/mock-helper.find-instances.ts @@ -25,11 +25,15 @@ export default (...args: any[]): T[] => { for (const element of elements) { mockHelperCrawl( element, - node => { + (node, parent) => { if (scanned.indexOf(node) === -1) { funcGetFromNode(result, node, declaration); scanned.push(node); } + if (parent && parent.nativeNode.nodeName === '#comment' && scanned.indexOf(parent) === -1) { + funcGetFromNode(result, parent, declaration); + scanned.push(parent); + } }, true, ); diff --git a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-injector.ts b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-injector.ts index a701e528e1..8bfcfe8e29 100644 --- a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-injector.ts +++ b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-injector.ts @@ -19,12 +19,8 @@ const getParentWithInjector = (node: (DebugNode & Node) | null): Injector | unde return undefined; }; -export default ( - result: T[], - node: (DebugNode & Node) | null | undefined, - proto: Type | InjectionToken, -): void => { - if (!node?.injector || node.injector.constructor.name === 'NullInjector') { +export default (result: T[], node: DebugNode & Node, proto: Type | InjectionToken): void => { + if (!node.injector || node.injector.constructor.name === 'NullInjector') { return; } diff --git a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts index 57830aa492..757ed1f317 100644 --- a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts +++ b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts @@ -3,6 +3,17 @@ import { DebugNode } from '@angular/core'; import { Type } from '../common/core.types'; const detectGatherFlag = (gather: boolean, el: DebugNode | null, node: any): boolean => { + // LContainer for structural directives can be a trigger for pipes. + if ( + el && + el.nativeNode && + el.nativeNode.nodeName === '#comment' && + Array.isArray(node) && + node[0] === el.nativeNode + ) { + return true; + } + // LContainer should stop the scan. if (Array.isArray(node)) { return false; @@ -12,6 +23,12 @@ const detectGatherFlag = (gather: boolean, el: DebugNode | null, node: any): boo return gather; } + // checking if a commentNode belongs to the current element. + // it comes from structural directives. + if (node.nodeName === '#comment') { + return node === el.nativeNode; + } + // checking if a textNode belongs to the current element. if (node.nodeName === '#text') { return node.parentNode === el.nativeNode; @@ -44,8 +61,13 @@ const scan = ( scanned.push(nodes); let gather = gatherDefault; - for (const raw of nodes) { - const node = normalize(raw); + let nodesLength = nodes.length; + if (nodes.length > 1 && nodes[1] && typeof nodes[1] === 'object' && nodes[1].bindingStartIndex) { + nodesLength = nodes[1].bindingStartIndex; + } + + for (let index = 0; index < nodesLength; index += 1) { + const node = normalize(nodes[index]); if (isNotObject(node)) { continue; } diff --git a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node.ts b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node.ts index 7253102d75..728e3cc7f5 100644 --- a/libs/ng-mocks/src/lib/mock-helper/func.get-from-node.ts +++ b/libs/ng-mocks/src/lib/mock-helper/func.get-from-node.ts @@ -28,11 +28,7 @@ export interface Node { parent?: (DebugNode & Node) | null; } -export default ( - result: T[], - node: (DebugNode & Node) | null | undefined, - proto: Type | InjectionToken, -): T[] => { +export default (result: T[], node: DebugNode & Node, proto: Type | InjectionToken): T[] => { funcGetFromNodeInjector(result, node, proto); if (!isNgDef(proto, 't')) { funcGetFromNodeStandard(result, node, proto); diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.spec.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.spec.ts deleted file mode 100644 index 5643944e17..0000000000 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MockRender, ngMocks } from 'ng-mocks'; - -import mockHelperGet from './mock-helper.get'; - -describe('mock-helper.get', () => { - class Proto {} - - it('searches in ViewEngine comments of structural directives', () => { - const proto = new Proto(); - MockRender('
'); - const span = ngMocks.find('span'); - const div = ngMocks.find('div'); - - // creating a comment like in the old View Engine - span.nativeElement.innerHTML = ''; - const comment = span.childNodes[0]; - div.nativeNode.parentNode.insertBefore( - comment.nativeNode, - div.nativeNode, - ); - - // we need to make it static - if (div.parent) { - ngMocks.stubMember(div, 'parent', div.parent); - ngMocks.stubMember( - div.parent, - 'queryAllNodes', - (filter: (item: any) => boolean) => - [ - { - _debugContext: { - view: { - nodes: [ - { - instance: proto, - }, - ], - }, - }, - nativeNode: comment.nativeNode, - } as any, - ].filter(filter), - ); - } - - const actual = mockHelperGet('div', Proto); - expect(actual).toBe(proto); - }); -}); diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts index 50cd4617b7..1afcada24b 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.get.ts @@ -1,3 +1,5 @@ +import { DebugElement } from '@angular/core'; + import { Type } from '../common/core.types'; import { getSourceOfMock } from '../common/func.get-source-of-mock'; import { MockedDebugElement } from '../mock-render/types'; @@ -5,6 +7,7 @@ import { MockedDebugElement } from '../mock-render/types'; import mockHelperFind from './find/mock-helper.find'; import funcGetFromNode from './func.get-from-node'; import funcGetLastFixture from './func.get-last-fixture'; +import nestedCheckParent from './crawl/nested-check-parent'; const defaultNotFoundValue = {}; // simulating Symbol @@ -22,24 +25,28 @@ const parseArgs = ( export default (...args: any[]) => { const { el, sel, notFoundValue } = parseArgs(args); - const root = mockHelperFind(funcGetLastFixture(), el, undefined); + const root: DebugElement | undefined = mockHelperFind(funcGetLastFixture(), el, undefined); + const source = getSourceOfMock(sel); - const res1 = funcGetFromNode([], root, getSourceOfMock(sel)); - if (res1.length > 0) { - return res1[0]; + // Looking in the root. + if (root) { + const result = funcGetFromNode([], root, source); + if (result.length > 0) { + return result[0]; + } } - // Looking for related structural directive. - const prevNode = root?.nativeNode.previousSibling; - const matches = - !prevNode || prevNode.nodeName !== '#comment' || !root || !root.parent - ? [] - : root.parent.queryAllNodes(node => node.nativeNode === prevNode); - const matchedNode = matches[0]; - const res2 = funcGetFromNode([], matchedNode, getSourceOfMock(sel)); - if (res2.length > 0) { - return res2[0]; + // Looking for a related structural directive. + if (root) { + const parent = nestedCheckParent(root, undefined); + if (parent && parent.nativeNode.nodeName === '#comment') { + const result = funcGetFromNode([], parent, source); + if (result.length > 0) { + return result[0]; + } + } } + if (notFoundValue !== defaultNotFoundValue) { return notFoundValue; } diff --git a/tests/issue-2314/test.spec.ts b/tests/issue-2314/test.spec.ts new file mode 100644 index 0000000000..f03f310198 --- /dev/null +++ b/tests/issue-2314/test.spec.ts @@ -0,0 +1,165 @@ +import { AsyncPipe, DatePipe, NgIf } from '@angular/common'; +import { Component, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BehaviorSubject } from 'rxjs'; + +import { ngMocks } from 'ng-mocks'; + +@Pipe({ + name: 'nothing', + pure: false, +}) +class NothingPipe implements PipeTransform { + transform(value: T): T { + return value; + } +} + +@Component({ + selector: 'target', + template: ` +
+ item: {{ item }} +
+
+ false +
+
+ {{ text$ | nothing | async | nothing | nothing }} +
+
+ true +
+ `, +}) +export class TargetComponent { + public array$ = new BehaviorSubject([1]); + public false$ = new BehaviorSubject(false); + public text$ = new BehaviorSubject('text'); + public true$ = new BehaviorSubject(true); +} + +// @see https://github.com/ike18t/ng-mocks/issues/2314 +describe('issue-2314', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + return TestBed.configureTestingModule({ + declarations: [TargetComponent, NothingPipe], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + }); + + it(`finds all pipes`, () => { + const allPipes = ngMocks.findInstances(AsyncPipe); + expect(allPipes.length).toBe(4); + }); + + describe('ngMocks.findInstance', () => { + it(`finds pipes on '.array'`, () => { + const arrayPipe = ngMocks.findInstance( + '.array', + AsyncPipe, + undefined, + ); + expect(arrayPipe).toBeDefined(); + }); + + it(`finds pipes on '.false'`, () => { + // Because it isn't rendered, we cannot find the element with `.false`, therefore, we need to rely on NgIf itself. + const ngIf = ngMocks.reveal(NgIf); + const falsePipe = ngMocks.findInstance( + ngIf, + AsyncPipe, + undefined, + ); + expect(falsePipe).toBeDefined(); + }); + + it(`find pipes on '.text'`, () => { + const textPipe = ngMocks.findInstance( + '.text', + AsyncPipe, + undefined, + ); + expect(textPipe).toBeDefined(); + }); + + it(`finds pipes on '.true'`, () => { + const truePipe = ngMocks.findInstance( + '.true', + AsyncPipe, + undefined, + ); + expect(truePipe).toBeDefined(); + }); + }); + + describe('ngMocks.findInstances', () => { + it(`finds pipes on '.array'`, () => { + const arrayPipe = ngMocks.findInstances('.array', AsyncPipe); + expect(arrayPipe.length).toEqual(1); + }); + + it(`finds pipes on '.false'`, () => { + // Because it isn't rendered, we cannot find the element with `.false`, therefore, we need to rely on NgIf itself. + const ngIf = ngMocks.reveal(NgIf); + const falsePipe = ngMocks.findInstances(ngIf, AsyncPipe); + expect(falsePipe.length).toEqual(1); + }); + + it(`find pipes on '.text'`, () => { + const textPipe = ngMocks.findInstances('.text', AsyncPipe); + expect(textPipe.length).toEqual(1); + }); + + it(`finds pipes on '.true'`, () => { + const truePipe = ngMocks.findInstances('.true', AsyncPipe); + expect(truePipe.length).toEqual(1); + }); + }); + + describe('ngMocks.get', () => { + it(`finds pipes on '.array'`, () => { + const arrayPipe = ngMocks.get('.array', AsyncPipe, undefined); + expect(arrayPipe).toBeDefined(); + }); + + it(`finds pipes on '.false'`, () => { + // Because it isn't rendered, we cannot find the element with `.false`, therefore, we need to rely on NgIf itself. + const ngIf = ngMocks.reveal(NgIf); + const falsePipe = ngMocks.get(ngIf, AsyncPipe, undefined); + expect(falsePipe).toBeDefined(); + }); + + it(`find pipes on '.text'`, () => { + const textPipe = ngMocks.get('.text', AsyncPipe, undefined); + expect(textPipe).toBeDefined(); + }); + + it(`finds pipes on '.true'`, () => { + const truePipe = ngMocks.get('.true', AsyncPipe, undefined); + expect(truePipe).toBeDefined(); + }); + + it(`returns default value when pipes are missing`, () => { + const truePipe = ngMocks.get('.true', DatePipe, undefined); + expect(truePipe).toBeUndefined(); + }); + }); +});