Skip to content

Commit

Permalink
feat: impure pipes support + .get, .findInstance can find them in…
Browse files Browse the repository at this point in the history
… fixtures

closes #240
  • Loading branch information
satanTime committed Dec 4, 2020
1 parent 84dc733 commit efa6337
Show file tree
Hide file tree
Showing 17 changed files with 590 additions and 33 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2435,7 +2435,7 @@ const directive = ngMocks.get(fixture.debugElement, Directive);

#### ngMocks.findInstance

Returns the first found attribute or structural directive which belongs to the current element or its any child.
Returns the first found component, directive, pipe or service which belongs to the current element or its any child.
If the element isn't specified then the current fixture is used.

- `ngMocks.findInstance( fixture?, directive, notFoundValue? )`
Expand All @@ -2448,11 +2448,13 @@ const directive3 = ngMocks.findInstance(
fixture.debugElement,
Directive3,
);
const pipe = ngMocks.findInstance(fixture.debugElement, MyPipe);
const service = ngMocks.findInstance(fixture, MyService);
```

#### ngMocks.findInstances

Returns an array of all found attribute or structural directives which belong to the current element and all its children.
Returns an array of all found components, directives, pipes or services which belong to the current element and all its children.
If the element isn't specified then the current fixture is used.

- `ngMocks.findInstances( fixture?, directive )`
Expand All @@ -2465,6 +2467,8 @@ const directives3 = ngMocks.findInstances(
fixture.debugElement,
Directive3,
);
const pipes = ngMocks.findInstances(fixture.debugElement, MyPipe);
const services = ngMocks.findInstance(fixture, MyService);
```

#### ngMocks.find
Expand Down
4 changes: 4 additions & 0 deletions lib/mock-helper/func.get-from-node-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { DebugNode } from '@angular/core';

export default (node: DebugNode): DebugNode =>
node.nativeNode.nodeName === '#text' && node.parent ? node.parent : node;
16 changes: 16 additions & 0 deletions lib/mock-helper/func.get-from-node-injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DebugNode } from '@angular/core';

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

import { Node } from './func.get-from-node';

export default <T>(result: T[], node: DebugNode & Node, proto: Type<T>): void => {
try {
const instance = node.injector.get(proto);
if (result.indexOf(instance) === -1) {
result.push(instance);
}
} catch (error) {
// nothing to do
}
};
88 changes: 88 additions & 0 deletions lib/mock-helper/func.get-from-node-ivy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import funcGetFromNodeIvy from './func.get-from-node-ivy';

describe('func.get-from-node-ivy', () => {
class Proto {}

it('finds parent context', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {},
parent: {
nativeNode: {
__ngContext__: [proto],
},
},
};

funcGetFromNodeIvy(result, node, Proto);

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

it('handles lView context', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {},
parent: {
nativeNode: {
__ngContext__: {
lView: [proto],
},
},
},
};

funcGetFromNodeIvy(result, node, Proto);

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

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

funcGetFromNodeIvy(result, node, Proto);

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

it('scans nested arrays', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
nativeNode: {},
parent: {
nativeNode: {
__ngContext__: [[[proto]]],
},
},
};

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

it('skips node with _debugContext', () => {
const result: any[] = [];
const proto = new Proto();
const node: any = {
_debugContext: {},
nativeNode: {},
parent: {
nativeNode: {
__ngContext__: [[[proto]]],
},
},
};

funcGetFromNodeIvy(result, node, Proto);
expect(result).toEqual([]);
});
});
39 changes: 39 additions & 0 deletions lib/mock-helper/func.get-from-node-ivy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DebugNode } from '@angular/core';

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

import { Node } from './func.get-from-node';
import funcGetFromNodeElement from './func.get-from-node-element';
import funcGetFromNodeScan from './func.get-from-node-scan';

const detectContext = (node: DebugNode): any => {
let current = node;
let context = current.nativeNode.__ngContext__;
while (!context && current.parent) {
current = current.parent;
context = current.nativeNode.__ngContext__;
}

return context;
};

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

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

const el = funcGetFromNodeElement(node);

funcGetFromNodeScan(
{
el,
nodes: contextToNodes(detectContext(node)) || [],
normalize: item => item,
proto,
result,
},
true,
);
};
60 changes: 60 additions & 0 deletions lib/mock-helper/func.get-from-node-scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { DebugNode } from '@angular/core';

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

const detectGatherFlag = (gather: boolean, el: DebugNode | null, node: any): boolean => {
if (!el || !node.nodeName) {
return gather;
}

// checking if a textNode belongs to the current element.
if (node.nodeName === '#text') {
return node.parentNode === el.nativeNode;
}

// checking if an injectedNode belongs to the current element.
return node === el.nativeNode;
};

const scan = <T>(
{
result,
el,
nodes,
normalize,
proto,
}: {
el: DebugNode | null;
nodes: any[];
normalize: (item: any) => any;
proto: Type<T>;
result: T[];
},
gatherDefault: boolean,
scanned: any[] = [],
): void => {
scanned.push(nodes);
let gather = gatherDefault;

for (const raw of nodes) {
const node = normalize(raw);
if (!node || typeof node !== 'object') {
continue;
}

if (scanned.indexOf(node) === -1 && Array.isArray(node)) {
scan({ result, el, nodes: node, normalize, proto }, gather, scanned);
}

gather = detectGatherFlag(gather, el, node);
if (!gather) {
continue;
}

if (result.indexOf(node) === -1 && node instanceof proto) {
result.push(node);
}
}
};

export default (() => scan)();
40 changes: 40 additions & 0 deletions lib/mock-helper/func.get-from-node-standard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { DebugNode } from '@angular/core';

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

import { Node } from './func.get-from-node';
import funcGetFromNodeElement from './func.get-from-node-element';
import funcGetFromNodeScan from './func.get-from-node-scan';

const normalize = (item: any): any => {
if (!item || typeof item !== 'object') {
return item;
}

for (const key of ['renderElement', 'renderText', 'instance']) {
if (item[key]) {
return item[key];
}
}

return null;
};

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

const el = funcGetFromNodeElement(node);

funcGetFromNodeScan(
{
el,
nodes: node._debugContext.view.nodes,
normalize,
proto,
result,
},
true,
);
};
36 changes: 36 additions & 0 deletions lib/mock-helper/func.get-from-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DebugNode } from '@angular/core';

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

import funcGetFromNodeInjector from './func.get-from-node-injector';
import funcGetFromNodeIvy from './func.get-from-node-ivy';
import funcGetFromNodeStandard from './func.get-from-node-standard';

export interface Node {
_debugContext?: {
elDef: {
nodeIndex: number;
};
nodeDef: {
nodeIndex: number;
};
nodeIndex: number;
view: {
nodes: Array<{
instance?: any;
renderElement?: any;
renderText?: any;
value?: any;
}>;
};
};
parent?: (DebugNode & Node) | null;
}

export default <T>(result: T[], node: DebugNode & Node, proto: Type<T>): T[] => {
funcGetFromNodeInjector(result, node, proto);
funcGetFromNodeStandard(result, node, proto);
funcGetFromNodeIvy(result, node, proto);

return result;
};
29 changes: 10 additions & 19 deletions lib/mock-helper/mock-helper.find-instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ import { Type } from '../common/core.types';
import { getSourceOfMock } from '../common/func.get-source-of-mock';
import { MockedDebugNode } from '../mock-render/types';

import funcGetFromNode from './func.get-from-node';
import funcGetLastFixture from './func.get-last-fixture';
import funcParseFindArgs from './func.parse-find-args';

function nestedCheck<T>(
result: T[],
node: MockedDebugNode & { childNodes?: MockedDebugNode[] },
callback: (node: MockedDebugNode) => undefined | T,
) {
const element = callback(node);
if (element) {
result.push(element);
}
const childNodes = node?.childNodes || [];
for (const childNode of childNodes) {
nestedCheck(result, childNode, callback);
interface DebugNode {
childNodes?: MockedDebugNode[];
}

function nestedCheck<T>(result: T[], node: MockedDebugNode & DebugNode, proto: Type<T>) {
funcGetFromNode(result, node, proto);
for (const childNode of node?.childNodes || []) {
nestedCheck(result, childNode, proto);
}
}

Expand All @@ -25,13 +22,7 @@ export default <T>(...args: any[]): T[] => {
const debugElement = el || funcGetLastFixture()?.debugElement;

const result: T[] = [];
nestedCheck<T>(result, debugElement, node => {
try {
return node.injector.get(getSourceOfMock(sel));
} catch (error) {
return undefined;
}
});
nestedCheck<T>(result, debugElement, getSourceOfMock(sel));

return result;
};

0 comments on commit efa6337

Please sign in to comment.