Debug element (node)
DebugElement
utility class is commonly used during unit testing to represent a DOM node and its associated entities (directives etc.).
Angular docs shows variations of its usage.
Here’s some example from the docs:
beforeEach(() => {
fixture.detectChanges(); // trigger initial data binding
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));
// get attached link directive instances
// using each DebugElement's injector
routerLinks = linkDes.map((de) => de.injector.get(RouterLink));
});
There’s also an internal spec
change_detection_integration
with lots of use cases. Here, for example, DebugElement
is used to retrieve directives attached (associated) to a DOM element:
describe('change notification', () => {
describe('updating directives', () => {
it('should happen without invoking the renderer', fakeAsync(() => {
const ctx = createCompFixture('<div testDirective [a]="42"></div>');
const nodes = ctx.debugElement.queryAllNodes(By.directive(dirType));
const directives = nodes.map(node => node.injector.get(TestDirective))
expect(directives[0].a).toEqual(42);
}));
});
DebugElement
is used in tests to verify particular state with DOM.
To the that end, it exposes a bunch of methods and properties.
Those methods are tested internally by Angular in the
debug_node_spec.ts:
it('should list all child nodes', () => {...})
it('should list all component child elements', () => {...})
it('should list conditional component child elements', () => {...})
it('should list child elements within viewports', () => {...})
it('should list element attributes', () => {...})
it('should list element classes', () => {...})
it('should list providerTokens', () => {...})
it('should get element classes from host bindings', () => {...})
it('should query child elements by css', () => {...})
it('should query child elements by directive', () => {...})
it('should query projected child elements by directive', () => {...})
it('should support querying on any debug element', () => {...})
As you can see, most of the API is designed to make it easy to verify relationships between nodes.
You could either check it directly or query it using
By
utility. If you’re interested in the full capabilities of DebugElement
related to unit-testing,
you can explore the spec to find various applications of the API.
Non-testing usage
Under the hood DebugElement
is a wrapper around a DOM node. It’s created inside the
getDebugNode
function at the moment of query:
export function getDebugNode(nativeNode: any): DebugNode|null {
if (nativeNode instanceof Node) {
if (!(nativeNode.hasOwnProperty(NG_DEBUG_PROPERTY))) {
(nativeNode as any)[NG_DEBUG_PROPERTY] = nativeNode.nodeType == Node.ELEMENT_NODE ?
new DebugElement(nativeNode as Element) :
new DebugNode(nativeNode);
}
return (nativeNode as any)[NG_DEBUG_PROPERTY];
}
return null;
}
To obtain the reference to this element, we need to pass the DOM node into the function getDebugElement
.
The function is public API, but it’s not added to the global namespace. We could attach the function to
the window
global object manually like this:
platformBrowserDynamic().bootstrapModule(AppModule)
.then(ref => {
(window as any).getDebugNode = getDebugNode;
})
.catch(err => console.error(err));
After that, the function could be accessed in the console:
const childComponentHostEl = document.querySelector('child-cmp');
const debugEl = window.getDebugNode(childComponentHostEl);
Some of the methods exposed on the DebugElement
use the global discovery utility methods we explored
in the previous section.
For example, here’s how the context
getter uses getComponent
and getContext
utilities:
get context(): any {
return getComponent(this.nativeNode) || getContext(this.nativeNode);
}
The getter returns a parent context for the element, which is often an ancestor component instance
that governs this element. When an element is part of an embedded view, e.g. repeated within *ngFor
,
the context is the embedded view context. For structural directive *ngFor
,
the context is passed through NgForOf
whose $implicit
property is the value of the row instance value.
For example, the hero
in *ngFor="let hero of heroes"
.
More discovery utils
In the context of debugging techniques, the primary interest of the API exposed through DebugElement
is to see which discovery utils it exposes
that are not part of global ng
namespace. There are two getters that might be useful for us:
- references
- providerTokens
The references
getter exposes a dictionary of objects associated with template local variables (e.g. #foo
),
keyed by the variable name used in a template. The providerTokens
getter exposes a component's injector lookup tokens.
This includes the component itself plus the tokens that the component lists in its providers metadata.
Here’s an example to illustrate the usage. We have a component that exposes ApplicationRef
token
and defines a template with a local variable f
that marks the div
element:
@Component({
selector: 'app-root',
template: '<div custom #f>Just a plain div with a local variable</div>',
providers: [{ provide: ApplicationRef, useExisting: ApplicationRef }],
})
export class AppComponent {}
Once we obtain the reference to the DebugElement
we can list the provider tokens like this:
As you might know, each component and directive adds itself to an element injector.
That’s why we see AppComponent
instance alongside ApplicationRef
token.
And here’s how we can see local references created through the #f
template syntax:
The rest of the API seems straightforward and self-explanatory.
It uses all discovery utility methods we explored before. Here’s the relevant part of the API for a quick reference:
export class DebugNode {
nativeNode
get listeners() {}
get context() {}
get injector() {}
get parent() {}
get references() {}
get providerTokens() {}
}
export class DebugElement extends DebugNode {
get nativeElement(): any {}
get name() {}
get properties() {}
get attributes() {}
get styles() {}
get classes() {}
get childNodes() {}
get children() {}
// Returns the first `DebugElement` that matches the predicate at any depth in the subtree.
query(predicate: Predicate<DebugElement>): DebugElement {
const results = this.queryAll(predicate);
return results[0] || null;
}
// Returns all `DebugElement` matches for the predicate at any depth in the subtree.
queryAll(predicate: Predicate<DebugElement>): DebugElement[] {
const matches: DebugElement[] = [];
_queryAll(this, predicate, matches, true);
return matches;
}
// Returns all `DebugNode` matches for the predicate at any depth in the subtree.
queryAllNodes(predicate: Predicate<DebugNode>): DebugNode[] {
const matches: DebugNode[] = [];
_queryAll(this, predicate, matches, false);
return matches;
}
/**
* Triggers the event by its name if there is a corresponding listener in the element's
* `listeners` collection.
*
* If the event lacks a listener or there's some other problem, consider
* calling `nativeElement.dispatchEvent(eventObject)`.
*
* @param eventName The name of the event to trigger
* @param eventObj The _event object_ expected by the handler
*
* @see [Testing components scenarios](guide/testing-components-scenarios#trigger-event-handler)
*/
triggerEventHandler(eventName: string, eventObj?: any): void {
const node = this.nativeNode as any;
const invokedListeners: Function[] = [];
this.listeners.forEach(listener => {
if (listener.name === eventName) {
const callback = listener.callback;
callback.call(node, eventObj);
invokedListeners.push(callback);
}
});
// We need to check whether `eventListeners` exists, because it's something
// that Zone.js only adds to `EventTarget` in browser environments.
if (typeof node.eventListeners === 'function') {
// Note that in Ivy we wrap event listeners with a call to `event.preventDefault` in some
// cases. We use '__ngUnwrap__' as a special token that gives us access to the actual event
// listener.
node.eventListeners(eventName).forEach((listener: Function) => {
// In order to ensure that we can detect the special __ngUnwrap__ token described above, we
// use `toString` on the listener and see if it contains the token. We use this approach to
// ensure that it still worked with compiled code since it cannot remove or rename string
// literals. We also considered using a special function name (i.e. if(listener.name ===
// special)) but that was more cumbersome and we were also concerned the compiled code could
// strip the name, turning the condition in to ("" === "") and always returning true.
if (listener.toString().indexOf('__ngUnwrap__') !== -1) {
const unwrappedListener = listener('__ngUnwrap__');
return invokedListeners.indexOf(unwrappedListener) === -1 &&
unwrappedListener.call(node, eventObj);
}
});
}
}