Skip to content

Commit c412374

Browse files
marclavalmhevery
authored andcommitted
fix(ivy): DebugNode.query should query nodes in the logical tree (angular#29480)
PR Close angular#29480
1 parent 9724247 commit c412374

File tree

15 files changed

+339
-205
lines changed

15 files changed

+339
-205
lines changed

packages/bazel/test/ng_package/core_package.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {fixmeIvy, ivyEnabled, obsoleteInIvy} from '@angular/private/testing';
9+
import {ivyEnabled, obsoleteInIvy} from '@angular/private/testing';
1010
import * as path from 'path';
1111
import * as shx from 'shelljs';
1212

packages/compiler-cli/test/ngtools_api_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {__NGTOOLS_PRIVATE_API_2 as NgTools_InternalApi_NG_2} from '@angular/compiler-cli';
10-
import {fixmeIvy, ivyEnabled} from '@angular/private/testing';
10+
import {ivyEnabled} from '@angular/private/testing';
1111
import * as path from 'path';
1212
import * as ts from 'typescript';
1313

packages/core/src/debug/debug_node.ts

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88

99
import {Injector} from '../di';
1010
import {getViewComponent} from '../render3/global_utils_api';
11-
import {TNode} from '../render3/interfaces/node';
11+
import {LContainer, NATIVE, VIEWS} from '../render3/interfaces/container';
12+
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../render3/interfaces/node';
1213
import {StylingIndex} from '../render3/interfaces/styling';
13-
import {LView, TData, TVIEW} from '../render3/interfaces/view';
14+
import {LView, NEXT, PARENT, TData, TVIEW, T_HOST} from '../render3/interfaces/view';
1415
import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings';
1516
import {getStylingContext} from '../render3/styling/util';
1617
import {getComponent, getContext, getInjectionTokens, getInjector, getListeners, getLocalRefs, isBrowserEvents, loadLContext, loadLContextFromNode} from '../render3/util/discovery_utils';
1718
import {INTERPOLATION_DELIMITER, isPropMetadataString, renderStringify} from '../render3/util/misc_utils';
19+
import {findComponentView} from '../render3/util/view_traversal_utils';
20+
import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer} from '../render3/util/view_utils';
1821
import {assertDomNode} from '../util/assert';
1922
import {DebugContext} from '../view/index';
2023

@@ -368,13 +371,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
368371

369372
queryAll(predicate: Predicate<DebugElement>): DebugElement[] {
370373
const matches: DebugElement[] = [];
371-
_queryNodeChildrenR3(this, predicate, matches, true);
374+
_queryAllR3(this, predicate, matches, true);
372375
return matches;
373376
}
374377

375378
queryAllNodes(predicate: Predicate<DebugNode>): DebugNode[] {
376379
const matches: DebugNode[] = [];
377-
_queryNodeChildrenR3(this, predicate, matches, false);
380+
_queryAllR3(this, predicate, matches, false);
378381
return matches;
379382
}
380383

@@ -387,20 +390,130 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
387390
}
388391
}
389392

393+
/**
394+
* Walk the TNode tree to find matches for the predicate, skipping the parent element.
395+
*
396+
* @param parentElement the element from which the walk is started
397+
* @param predicate the predicate to match
398+
* @param matches the list of positive matches
399+
* @param elementsOnly whether only elements should be searched
400+
*/
401+
function _queryAllR3(
402+
parentElement: DebugElement, predicate: Predicate<DebugNode>, matches: DebugNode[],
403+
elementsOnly: boolean) {
404+
const context = loadLContext(parentElement.nativeNode) !;
405+
const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode;
406+
// This the fixture's debug element, so this is always a component view.
407+
const lView = context.lView[parentTNode.index];
408+
const tNode = lView[TVIEW].firstChild;
409+
_queryNodeChildrenR3(tNode, lView, predicate, matches, elementsOnly);
410+
}
411+
412+
/**
413+
* Recursively match the current TNode against the predicate, and goes on with the next ones.
414+
*
415+
* @param tNode the current TNode
416+
* @param lView the LView of this TNode
417+
* @param predicate the predicate to match
418+
* @param matches the list of positive matches
419+
* @param elementsOnly whether only elements should be searched
420+
*/
390421
function _queryNodeChildrenR3(
391-
parentNode: DebugNode, predicate: Predicate<DebugNode>, matches: DebugNode[],
422+
tNode: TNode, lView: LView, predicate: Predicate<DebugNode>, matches: DebugNode[],
392423
elementsOnly: boolean) {
393-
if (parentNode instanceof DebugElement__POST_R3__) {
394-
parentNode.childNodes.forEach(node => {
395-
if (predicate(node)) {
396-
matches.push(node);
424+
// For each type of TNode, specific logic is executed.
425+
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) {
426+
// Case 1: the TNode is an element
427+
// The native node has to be checked.
428+
_addQueryMatchR3(getNativeByTNode(tNode, lView), predicate, matches, elementsOnly);
429+
if (isComponent(tNode)) {
430+
// If the element is the host of a component, then all nodes in its view have to be processed.
431+
// Note: the component's content (tNode.child) will be processed from the insertion points.
432+
const componentView = getComponentViewByIndex(tNode.index, lView);
433+
if (componentView && componentView[TVIEW].firstChild)
434+
_queryNodeChildrenR3(
435+
componentView[TVIEW].firstChild !, componentView, predicate, matches, elementsOnly);
436+
} else {
437+
// Otherwise, its children have to be processed.
438+
if (tNode.child) _queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly);
439+
}
440+
// In all cases, if a dynamic container exists for this node, each view inside it has to be
441+
// processed.
442+
const nodeOrContainer = lView[tNode.index];
443+
if (isLContainer(nodeOrContainer)) {
444+
_queryNodeChildrenInContainerR3(nodeOrContainer, predicate, matches, elementsOnly);
445+
}
446+
} else if (tNode.type === TNodeType.Container) {
447+
// Case 2: the TNode is a container
448+
// The native node has to be checked.
449+
const lContainer = lView[tNode.index];
450+
_addQueryMatchR3(lContainer[NATIVE], predicate, matches, elementsOnly);
451+
// Each view inside the container has to be processed.
452+
_queryNodeChildrenInContainerR3(lContainer, predicate, matches, elementsOnly);
453+
} else if (tNode.type === TNodeType.Projection) {
454+
// Case 3: the TNode is a projection insertion point (i.e. a <ng-content>).
455+
// The nodes projected at this location all need to be processed.
456+
const componentView = findComponentView(lView !);
457+
const componentHost = componentView[T_HOST] as TElementNode;
458+
const head: TNode|null =
459+
(componentHost.projection as(TNode | null)[])[tNode.projection as number];
460+
461+
if (Array.isArray(head)) {
462+
for (let nativeNode of head) {
463+
_addQueryMatchR3(nativeNode, predicate, matches, elementsOnly);
397464
}
398-
if (node instanceof DebugElement__POST_R3__) {
399-
if (elementsOnly ? node.nativeElement : true) {
400-
_queryNodeChildrenR3(node, predicate, matches, elementsOnly);
401-
}
465+
} else {
466+
if (head) {
467+
const nextLView = componentView[PARENT] !as LView;
468+
const nextTNode = nextLView[TVIEW].data[head.index] as TNode;
469+
_queryNodeChildrenR3(nextTNode, nextLView, predicate, matches, elementsOnly);
402470
}
403-
});
471+
}
472+
} else {
473+
// Case 4: the TNode is a view.
474+
if (tNode.child) {
475+
_queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly);
476+
}
477+
}
478+
// To determine the next node to be processed, we need to use the next or the projectionNext link,
479+
// depending on whether the current node has been projected.
480+
const nextTNode = (tNode.flags & TNodeFlags.isProjected) ? tNode.projectionNext : tNode.next;
481+
if (nextTNode) {
482+
_queryNodeChildrenR3(nextTNode, lView, predicate, matches, elementsOnly);
483+
}
484+
}
485+
486+
/**
487+
* Process all TNodes in a given container.
488+
*
489+
* @param lContainer the container to be processed
490+
* @param predicate the predicate to match
491+
* @param matches the list of positive matches
492+
* @param elementsOnly whether only elements should be searched
493+
*/
494+
function _queryNodeChildrenInContainerR3(
495+
lContainer: LContainer, predicate: Predicate<DebugNode>, matches: DebugNode[],
496+
elementsOnly: boolean) {
497+
for (let i = 0; i < lContainer[VIEWS].length; i++) {
498+
const childView = lContainer[VIEWS][i];
499+
_queryNodeChildrenR3(childView[TVIEW].node !, childView, predicate, matches, elementsOnly);
500+
}
501+
}
502+
503+
/**
504+
* Match the current native node against the predicate.
505+
*
506+
* @param nativeNode the current native node
507+
* @param predicate the predicate to match
508+
* @param matches the list of positive matches
509+
* @param elementsOnly whether only elements should be searched
510+
*/
511+
function _addQueryMatchR3(
512+
nativeNode: any, predicate: Predicate<DebugNode>, matches: DebugNode[], elementsOnly: boolean) {
513+
const debugNode = getDebugNode(nativeNode);
514+
if (debugNode && (elementsOnly ? debugNode instanceof DebugElement__POST_R3__ : true) &&
515+
predicate(debugNode)) {
516+
matches.push(debugNode);
404517
}
405518
}
406519

packages/core/test/animation/animation_query_integration_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {CommonModule} from '@angular/common';
1414
import {Component, HostBinding, ViewChild} from '@angular/core';
1515
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
1616
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
17-
import {fixmeIvy, ivyEnabled} from '@angular/private/testing';
17+
import {ivyEnabled} from '@angular/private/testing';
1818

1919
import {HostListener} from '../../src/metadata/directives';
2020

packages/core/test/animation/animation_router_integration_spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/brow
1111
import {Component, HostBinding} from '@angular/core';
1212
import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
1313
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
14-
import {fixmeIvy} from '@angular/private/testing';
1514
import {ActivatedRoute, Router, RouterOutlet} from '@angular/router';
1615
import {RouterTestingModule} from '@angular/router/testing';
1716

packages/core/test/debug/debug_node_spec.ts

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99

10-
import {Component, Directive, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA} from '@angular/core';
10+
import {Component, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
1111
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
1212
import {By} from '@angular/platform-browser/src/dom/debug/by';
1313
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@@ -154,6 +154,15 @@ class BankAccount {
154154
normalizedBankName !: string;
155155
}
156156

157+
@Component({
158+
template: `
159+
<div class="content" #content>Some content</div>
160+
`
161+
})
162+
class SimpleContentComp {
163+
@ViewChild('content') content !: ElementRef;
164+
}
165+
157166
@Component({
158167
selector: 'test-app',
159168
template: `
@@ -202,6 +211,7 @@ class HostClassBindingCmp {
202211
BankAccount,
203212
TestCmpt,
204213
HostClassBindingCmp,
214+
SimpleContentComp,
205215
],
206216
providers: [Logger],
207217
schemas: [NO_ERRORS_SCHEMA],
@@ -369,6 +379,122 @@ class HostClassBindingCmp {
369379
expect(debugElement).toBeTruthy();
370380
});
371381

382+
it('should query re-projected child elements by directive', () => {
383+
@Directive({selector: 'example-directive-a'})
384+
class ExampleDirectiveA {
385+
}
386+
387+
@Component({
388+
selector: 'proxy-component',
389+
template: `
390+
<ng-content></ng-content>
391+
`
392+
})
393+
class ProxyComponent {
394+
}
395+
396+
@Component({
397+
selector: 'wrapper-component',
398+
template: `
399+
<proxy-component>
400+
<ng-content select="div"></ng-content>
401+
<ng-content select="example-directive-a"></ng-content>
402+
</proxy-component>
403+
`
404+
})
405+
class WrapperComponent {
406+
}
407+
408+
TestBed.configureTestingModule({
409+
declarations: [
410+
ProxyComponent,
411+
WrapperComponent,
412+
ExampleDirectiveA,
413+
]
414+
});
415+
416+
TestBed.overrideTemplate(TestApp, `<wrapper-component>
417+
<div></div>
418+
<example-directive-a></example-directive-a>
419+
</wrapper-component>`);
420+
421+
const fixture = TestBed.createComponent(TestApp);
422+
fixture.detectChanges();
423+
424+
const debugElements = fixture.debugElement.queryAll(By.directive(ExampleDirectiveA));
425+
expect(debugElements.length).toBe(1);
426+
});
427+
428+
it('should query directives on containers before directives in a view', () => {
429+
@Directive({selector: '[text]'})
430+
class TextDirective {
431+
@Input() text: string|undefined;
432+
}
433+
434+
TestBed.configureTestingModule({declarations: [TextDirective]});
435+
TestBed.overrideTemplate(
436+
TestApp,
437+
`<ng-template text="first" [ngIf]="true"><div text="second"></div></ng-template>`);
438+
439+
const fixture = TestBed.createComponent(TestApp);
440+
fixture.detectChanges();
441+
442+
const debugNodes = fixture.debugElement.queryAllNodes(By.directive(TextDirective));
443+
expect(debugNodes.length).toBe(2);
444+
expect(debugNodes[0].injector.get(TextDirective).text).toBe('first');
445+
expect(debugNodes[1].injector.get(TextDirective).text).toBe('second');
446+
});
447+
448+
it('should query directives on views moved in the DOM', () => {
449+
@Directive({selector: '[text]'})
450+
class TextDirective {
451+
@Input() text: string|undefined;
452+
}
453+
454+
@Directive({selector: '[moveView]'})
455+
class ViewManipulatingDirective {
456+
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<any>) {}
457+
458+
insert() { this._vcRef.createEmbeddedView(this._tplRef); }
459+
460+
removeFromTheDom() {
461+
const viewRef = this._vcRef.get(0) as EmbeddedViewRef<any>;
462+
viewRef.rootNodes.forEach(rootNode => { getDOM().remove(rootNode); });
463+
}
464+
}
465+
466+
TestBed.configureTestingModule({declarations: [TextDirective, ViewManipulatingDirective]});
467+
TestBed.overrideTemplate(
468+
TestApp, `<ng-template text="first" moveView><div text="second"></div></ng-template>`);
469+
470+
const fixture = TestBed.createComponent(TestApp);
471+
fixture.detectChanges();
472+
473+
const viewMover =
474+
fixture.debugElement.queryAllNodes(By.directive(ViewManipulatingDirective))[0]
475+
.injector.get(ViewManipulatingDirective);
476+
477+
let debugNodes = fixture.debugElement.queryAllNodes(By.directive(TextDirective));
478+
479+
// we've got just one directive on <ng-template>
480+
expect(debugNodes.length).toBe(1);
481+
expect(debugNodes[0].injector.get(TextDirective).text).toBe('first');
482+
483+
// insert a view - now we expect to find 2 directive instances
484+
viewMover.insert();
485+
fixture.detectChanges();
486+
debugNodes = fixture.debugElement.queryAllNodes(By.directive(TextDirective));
487+
expect(debugNodes.length).toBe(2);
488+
489+
// remove a view from the DOM (equivalent to moving it around)
490+
// the logical tree is the same but DOM has changed
491+
viewMover.removeFromTheDom();
492+
debugNodes = fixture.debugElement.queryAllNodes(By.directive(TextDirective));
493+
expect(debugNodes.length).toBe(2);
494+
expect(debugNodes[0].injector.get(TextDirective).text).toBe('first');
495+
expect(debugNodes[1].injector.get(TextDirective).text).toBe('second');
496+
});
497+
372498
it('should list providerTokens', () => {
373499
fixture = TestBed.createComponent(ParentComp);
374500
fixture.detectChanges();
@@ -489,5 +615,21 @@ class HostClassBindingCmp {
489615
});
490616
});
491617

618+
it('should be able to query for elements that are not in the same DOM tree anymore', () => {
619+
fixture = TestBed.createComponent(SimpleContentComp);
620+
fixture.detectChanges();
621+
622+
const parent = getDOM().parentElement(fixture.nativeElement) !;
623+
const content = fixture.componentInstance.content.nativeElement;
624+
625+
// Move the content element outside the component
626+
// so that it can't be reached via querySelector.
627+
getDOM().appendChild(parent, content);
628+
629+
expect(fixture.debugElement.query(By.css('.content'))).toBeTruthy();
630+
631+
getDOM().remove(content);
632+
});
633+
492634
});
493635
}

packages/core/test/linker/ng_module_integration_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {NgModuleData} from '@angular/core/src/view/types';
1414
import {tokenKey} from '@angular/core/src/view/util';
1515
import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
1616
import {expect} from '@angular/platform-browser/testing/src/matchers';
17-
import {fixmeIvy, modifiedInIvy, obsoleteInIvy, onlyInIvy} from '@angular/private/testing';
17+
import {modifiedInIvy, obsoleteInIvy, onlyInIvy} from '@angular/private/testing';
1818

1919
import {InternalNgModuleRef, NgModuleFactory} from '../../src/linker/ng_module_factory';
2020
import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader';

0 commit comments

Comments
 (0)