fix(ivy): Prevent errors when querying DebugElement roots that were outside angular context (#34687)

DebugElement.query also matches elements that may have been created
outside of Angular (ex: with `document.appendChild`). If those matched
DebugElements are in turn used to query for more elements, an error
occurs because the first step in queryAll is to load the LContext.

PR Close #34687
This commit is contained in:
Andrew Scott 2020-01-08 16:39:32 -08:00 committed by atscott
parent 58f10026c4
commit 7d401853b5
2 changed files with 58 additions and 30 deletions

View File

@ -470,10 +470,16 @@ function _queryAllR3(
function _queryAllR3( function _queryAllR3(
parentElement: DebugElement, predicate: Predicate<DebugElement>| Predicate<DebugNode>, parentElement: DebugElement, predicate: Predicate<DebugElement>| Predicate<DebugNode>,
matches: DebugElement[] | DebugNode[], elementsOnly: boolean) { matches: DebugElement[] | DebugNode[], elementsOnly: boolean) {
const context = loadLContext(parentElement.nativeNode) !; const context = loadLContext(parentElement.nativeNode, false);
const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode; if (context !== null) {
_queryNodeChildrenR3( const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode;
parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode); _queryNodeChildrenR3(
parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode);
} else {
// If the context is null, then `parentElement` was either created with Renderer2 or native DOM
// APIs.
_queryNativeNodeDescendants(parentElement.nativeNode, predicate, matches, elementsOnly);
}
} }
/** /**

View File

@ -577,38 +577,51 @@ class TestCmptWithPropInterpolation {
expect(fixture.debugElement.query(By.css('.myclass'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('.myclass'))).toBeTruthy();
}); });
it('DebugElement.query should work with dynamically created descendant elements', () => { describe('DebugElement.query with dynamically created descendant elements', () => {
@Directive({ let fixture: ComponentFixture<{}>;
selector: '[dir]', beforeEach(() => {
})
class MyDir {
@Input('dir') dir: number|undefined;
constructor(renderer: Renderer2, element: ElementRef) { @Directive({
const outerDiv = renderer.createElement('div'); selector: '[dir]',
const innerDiv = renderer.createElement('div'); })
const div = renderer.createElement('div'); class MyDir {
@Input('dir') dir: number|undefined;
div.classList.add('myclass'); constructor(renderer: Renderer2, element: ElementRef) {
const outerDiv = renderer.createElement('div');
const innerDiv = renderer.createElement('div');
innerDiv.classList.add('inner');
const div = renderer.createElement('div');
renderer.appendChild(innerDiv, div); div.classList.add('myclass');
renderer.appendChild(outerDiv, innerDiv);
renderer.appendChild(element.nativeElement, outerDiv); renderer.appendChild(innerDiv, div);
renderer.appendChild(outerDiv, innerDiv);
renderer.appendChild(element.nativeElement, outerDiv);
}
} }
}
@Component({ @Component({
selector: 'app-test', selector: 'app-test',
template: '<div dir></div>', template: '<div dir></div>',
}) })
class MyComponent { class MyComponent {
} }
TestBed.configureTestingModule({declarations: [MyComponent, MyDir]}); TestBed.configureTestingModule({declarations: [MyComponent, MyDir]});
const fixture = TestBed.createComponent(MyComponent); fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.myclass'))).toBeTruthy(); });
it('should find the dynamic elements from fixture root',
() => { expect(fixture.debugElement.query(By.css('.myclass'))).toBeTruthy(); });
it('can use a dynamic element as root for another query', () => {
const inner = fixture.debugElement.query(By.css('.inner'));
expect(inner).toBeTruthy();
expect(inner.query(By.css('.myclass'))).toBeTruthy();
});
}); });
describe('DebugElement.query doesn\'t fail on elements outside Angular context', () => { describe('DebugElement.query doesn\'t fail on elements outside Angular context', () => {
@ -617,7 +630,9 @@ class TestCmptWithPropInterpolation {
constructor(private elementRef: ElementRef) {} constructor(private elementRef: ElementRef) {}
ngAfterViewInit() { ngAfterViewInit() {
this.elementRef.nativeElement.children[0].appendChild(document.createElement('p')); const ul = document.createElement('ul');
ul.appendChild(document.createElement('li'));
this.elementRef.nativeElement.children[0].appendChild(ul);
} }
} }
@ -664,6 +679,13 @@ class TestCmptWithPropInterpolation {
it('when searching by injector', it('when searching by injector',
() => { expect(() => el.query(e => e.injector === null)).not.toThrow(); }); () => { expect(() => el.query(e => e.injector === null)).not.toThrow(); });
onlyInIvy('VE does not match elements created outside Angular context')
.it('when using the out-of-context element as the DebugElement query root', () => {
const debugElOutsideAngularContext = el.query(By.css('ul'));
expect(debugElOutsideAngularContext.queryAll(By.css('li')).length).toBe(1);
expect(debugElOutsideAngularContext.query(By.css('li'))).toBeDefined();
});
}); });
it('DebugElement.queryAll should pick up both elements inserted via the view and through Renderer2', it('DebugElement.queryAll should pick up both elements inserted via the view and through Renderer2',