Skip to content

Commit

Permalink
fix: correct stop of search in ivy tree
Browse files Browse the repository at this point in the history
closes #298
  • Loading branch information
satanTime committed Feb 13, 2021
1 parent 397ecf8 commit 809916d
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DebugNode } from '@angular/core';

export default (node: DebugNode): DebugNode =>
node.nativeNode.nodeName === '#text' && node.parent ? node.parent : node;
node.nativeNode?.nodeName === '#text' && node.parent ? node.parent : node;
86 changes: 86 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/func.get-from-node-ivy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { InjectionToken } from '@angular/core';

import funcGetFromNodeIvy from './func.get-from-node-ivy';

describe('func.get-from-node-ivy', () => {
Expand Down Expand Up @@ -39,6 +41,28 @@ describe('func.get-from-node-ivy', () => {
expect(result).toEqual([proto]);
});

it('disables on arrays', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {},
parent: {
nativeNode: {
__ngContext__: {
lView: [
[] /* now proto should not be collected */,
proto,
],
},
},
},
};

funcGetFromNodeIvy(result, node, Proto);

expect(result).toEqual([]);
});

it('handles empty context', () => {
const result: any[] = [];
const node: any = {
Expand Down Expand Up @@ -69,6 +93,68 @@ describe('func.get-from-node-ivy', () => {
expect(result).toEqual([proto]);
});

it('scans DebugElement', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {
__ng_debug__: {
injector: {
get: (classObject: any) =>
classObject === Proto ? proto : undefined,
},
providerTokens: [new InjectionToken('TOKEN'), Proto],
},
},
parent: {
nativeNode: {
__ngContext__: [],
},
},
};

funcGetFromNodeIvy(result, node, Proto);
expect(result).toEqual([proto]);
});

it('handles duplicates in DebugElement', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {
__ng_debug__: {
injector: {
get: (classObject: any) =>
classObject === Proto ? proto : undefined,
},
providerTokens: [new InjectionToken('TOKEN'), Proto, Proto],
},
},
parent: {
nativeNode: {
__ngContext__: [],
},
},
};

funcGetFromNodeIvy(result, node, Proto);
expect(result).toEqual([proto]);
});

it('handles missed nativeNode', () => {
const result: any[] = [];
const node: any = {
parent: {
nativeNode: {
__ngContext__: [],
},
},
};

funcGetFromNodeIvy(result, node, Proto);
expect(result).toEqual([]);
});

it('skips node with _debugContext', () => {
const result: any[] = [];
const proto = new Proto();
Expand Down
23 changes: 20 additions & 3 deletions libs/ng-mocks/src/lib/mock-helper/func.get-from-node-ivy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DebugNode } from '@angular/core';
import { DebugElement, DebugNode } from '@angular/core';

import { Type } from '../common/core.types';

Expand All @@ -8,7 +8,7 @@ import funcGetFromNodeScan from './func.get-from-node-scan';

const detectContext = (node: DebugNode): any => {
let current = node;
let context = current.nativeNode.__ngContext__;
let context = current.nativeNode?.__ngContext__;
while (!context && current.parent) {
current = current.parent;
context = current.nativeNode.__ngContext__;
Expand All @@ -19,13 +19,30 @@ const detectContext = (node: DebugNode): any => {

const contextToNodes = (context: any): any => (Array.isArray(context) ? context : context?.lView);

const getFromDebugElement = <T>(el: DebugNode, proto: Type<any>, result: T[]): void => {
const debugElement: DebugElement = el.nativeNode?.__ng_debug__;
for (const provider of debugElement?.providerTokens || []) {
if (provider !== proto) {
continue;
}
try {
const instance = debugElement.injector.get(provider);
if (instance && result.indexOf(instance) === -1) {
result.push(instance);
}
} catch (e) {
// nothing to do
}
}
};

export default <T>(result: T[], node: (DebugNode & Node) | null | undefined, proto: Type<T>): void => {
if (!node || node._debugContext) {
return;
}

const el = funcGetFromNodeElement(node);

getFromDebugElement(el, proto, result);
funcGetFromNodeScan(
{
el,
Expand Down
5 changes: 5 additions & 0 deletions libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { DebugNode } from '@angular/core';
import { Type } from '../common/core.types';

const detectGatherFlag = (gather: boolean, el: DebugNode | null, node: any): boolean => {
// LContainer should stop the scan.
if (Array.isArray(node)) {
return false;
}

if (!el || !node.nodeName) {
return gather;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const getMeta = (token: any): Directive | undefined => {
};

export default (el: DebugNode | null | undefined, token: any): Directive | undefined => {
// istanbul ignore if
if (!el) {
return undefined;
}
Expand Down
52 changes: 52 additions & 0 deletions tests-angular/e2e/src/stack-blitz.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Component, Directive, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MockDirective, ngMocks } from 'ng-mocks';

@Directive({
selector: '[myDirective]',
})
export class MyDirective {
@Input() public value?: string;
}

@Component({
selector: 'app',
template: `
<div class="p1"><span myDirective value="d1"></span></div>
<div class="p2"><span myDirective value="d2"></span></div>
<div class="p3"><span myDirective value="d3"></span></div>
`,
})
export class AppComponent {}

describe('issue-298:stack-blitz', () => {
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MockDirective(MyDirective), AppComponent],
});
});
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
});

it(`should be 'd1'`, () => {
const debugElement = fixture.debugElement.query(By.css('.p1'));
const directive = ngMocks.findInstance(debugElement, MyDirective);
expect(directive.value).toBe('d1');
});

it(`should be 'd2'`, () => {
const debugElement = fixture.debugElement.query(By.css('.p2'));
const directive = ngMocks.findInstance(debugElement, MyDirective);
expect(directive.value).toBe('d2');
});

it(`should be 'd3'`, () => {
const debugElement = fixture.debugElement.query(By.css('.p3'));
const directive = ngMocks.findInstance(debugElement, MyDirective);
expect(directive.value).toBe('d3');
});
});
163 changes: 163 additions & 0 deletions tests-angular/e2e/src/test.nodes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockRender, ngMocks } from 'ng-mocks';

@Pipe({
name: 'pure',
pure: true,
})
export class PurePipe implements PipeTransform {
public readonly name = 'PurePipe';
public value: any;

public transform(value: string): string {
this.value = value;

return `${this.name}:${value}`;
}
}

@Pipe({
name: 'impure',
pure: false,
})
export class ImpurePipe implements PipeTransform {
public readonly name = 'ImpurePipe';
public value: any;

public transform(value: string): string {
this.value = value;

return `${this.name}:${value}`;
}
}

describe('issue-240:nodes', () => {
beforeEach(() =>
TestBed.configureTestingModule({
declarations: [ImpurePipe],
}).compileComponents(),
);

it('calls pipes differently', () => {
MockRender(
`
<div class="parent">
"parent-1:{{ "parent-1" | impure }}"
<div class="child-1">
<!-- comment before -->
"child-1:{{ "child-1" | impure }}"
</div>
<div class="child-2">
"child-2:{{ "child-2" | impure }}"
<!-- comment after -->
<div class="child-3">
text before
"child-3:{{ "child-3" | impure }}"
</div>
</div>
<ng-container>
"parent-2:{{ "parent-2" | impure }}"
<div class="obstacle"></div>
<ng-container>
"parent-3:{{ "parent-3" | impure }}"
text after
</ng-container>
</ng-container>
</div>
`,
);

const parent = ngMocks.find('.parent');
const child1 = ngMocks.find('.child-1');
const child2 = ngMocks.find('.child-2');
const child3 = ngMocks.find('.child-3');

expect(parent.nativeElement.innerHTML).toContain(
'"parent-1:ImpurePipe:parent-1"',
);
expect(parent.nativeElement.innerHTML).toContain(
'"parent-2:ImpurePipe:parent-2"',
);
expect(parent.nativeElement.innerHTML).toContain(
'"child-1:ImpurePipe:child-1"',
);
expect(parent.nativeElement.innerHTML).toContain(
'"child-2:ImpurePipe:child-2"',
);
expect(parent.nativeElement.innerHTML).toContain(
'"child-3:ImpurePipe:child-3"',
);

expect(child1.nativeElement.innerHTML).not.toContain(
'"parent-1:ImpurePipe:parent-1"',
);
expect(child1.nativeElement.innerHTML).not.toContain(
'"parent-2:ImpurePipe:parent-2"',
);
expect(child1.nativeElement.innerHTML).toContain(
'"child-1:ImpurePipe:child-1"',
);
expect(child1.nativeElement.innerHTML).not.toContain(
'"child-2:ImpurePipe:child-2"',
);
expect(child1.nativeElement.innerHTML).not.toContain(
'"child-3:ImpurePipe:child-3"',
);

expect(child2.nativeElement.innerHTML).not.toContain(
'"parent-1:ImpurePipe:parent-1"',
);
expect(child2.nativeElement.innerHTML).not.toContain(
'"parent-2:ImpurePipe:parent-2"',
);
expect(child2.nativeElement.innerHTML).not.toContain(
'"child-1:ImpurePipe:child-1"',
);
expect(child2.nativeElement.innerHTML).toContain(
'"child-2:ImpurePipe:child-2"',
);
expect(child2.nativeElement.innerHTML).toContain(
'"child-3:ImpurePipe:child-3"',
);

expect(child3.nativeElement.innerHTML).not.toContain(
'"parent-1:ImpurePipe:parent-1"',
);
expect(child3.nativeElement.innerHTML).not.toContain(
'"parent-2:ImpurePipe:parent-2"',
);
expect(child3.nativeElement.innerHTML).not.toContain(
'"child-1:ImpurePipe:child-1"',
);
expect(child3.nativeElement.innerHTML).not.toContain(
'"child-2:ImpurePipe:child-2"',
);
expect(child3.nativeElement.innerHTML).toContain(
'"child-3:ImpurePipe:child-3"',
);

const parentPipes = ngMocks.findInstances(parent, ImpurePipe);
expect(parentPipes.map(item => item.value)).toEqual([
// all in the root node first
'parent-1',
'parent-2',
'parent-3',
'child-1',
'child-2',
'child-3',
]);

const child1Pipes = ngMocks.findInstances(child1, ImpurePipe);
expect(child1Pipes.map(item => item.value)).toEqual(['child-1']);

const child2Pipes = ngMocks.findInstances(child2, ImpurePipe);
expect(child2Pipes.map(item => item.value)).toEqual([
'child-2',
'child-3',
]);

const child3Pipes = ngMocks.findInstances(child3, ImpurePipe);
expect(child3Pipes.map(item => item.value)).toEqual(['child-3']);
});
});

0 comments on commit 809916d

Please sign in to comment.