From 7d401853b52472fc0c58670141a69caa7cbd7994 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 8 Jan 2020 16:39:32 -0800 Subject: [PATCH] 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 --- packages/core/src/debug/debug_node.ts | 14 ++-- packages/core/test/debug/debug_node_spec.ts | 74 +++++++++++++-------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index 86e3d66b18..0b1e81d402 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -470,10 +470,16 @@ function _queryAllR3( function _queryAllR3( parentElement: DebugElement, predicate: Predicate| Predicate, matches: DebugElement[] | DebugNode[], elementsOnly: boolean) { - const context = loadLContext(parentElement.nativeNode) !; - const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode; - _queryNodeChildrenR3( - parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode); + const context = loadLContext(parentElement.nativeNode, false); + if (context !== null) { + const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode; + _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); + } } /** diff --git a/packages/core/test/debug/debug_node_spec.ts b/packages/core/test/debug/debug_node_spec.ts index f86394c741..d3c22df2da 100644 --- a/packages/core/test/debug/debug_node_spec.ts +++ b/packages/core/test/debug/debug_node_spec.ts @@ -577,38 +577,51 @@ class TestCmptWithPropInterpolation { expect(fixture.debugElement.query(By.css('.myclass'))).toBeTruthy(); }); - it('DebugElement.query should work with dynamically created descendant elements', () => { - @Directive({ - selector: '[dir]', - }) - class MyDir { - @Input('dir') dir: number|undefined; + describe('DebugElement.query with dynamically created descendant elements', () => { + let fixture: ComponentFixture<{}>; + beforeEach(() => { - constructor(renderer: Renderer2, element: ElementRef) { - const outerDiv = renderer.createElement('div'); - const innerDiv = renderer.createElement('div'); - const div = renderer.createElement('div'); + @Directive({ + selector: '[dir]', + }) + 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); - renderer.appendChild(outerDiv, innerDiv); - renderer.appendChild(element.nativeElement, outerDiv); + div.classList.add('myclass'); + + renderer.appendChild(innerDiv, div); + renderer.appendChild(outerDiv, innerDiv); + renderer.appendChild(element.nativeElement, outerDiv); + } } - } - @Component({ - selector: 'app-test', - template: '
', - }) - class MyComponent { - } + @Component({ + selector: 'app-test', + template: '
', + }) + class MyComponent { + } - TestBed.configureTestingModule({declarations: [MyComponent, MyDir]}); - const fixture = TestBed.createComponent(MyComponent); - fixture.detectChanges(); + TestBed.configureTestingModule({declarations: [MyComponent, MyDir]}); + fixture = TestBed.createComponent(MyComponent); + 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', () => { @@ -617,7 +630,9 @@ class TestCmptWithPropInterpolation { constructor(private elementRef: ElementRef) {} 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', () => { 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',