Skip to content

Commit b4ed62d

Browse files
AleksanderBodurrikirjs
authored andcommitted
fix(core): properly handle the case where getSignalGraph is called on a componentless NodeInjector (#60772)
Previously this would throw an error on the assertLView when we try to discover the templateLView. Now this properly returns null for the template consumer and continues discovering other effects on the injector. PR Close #60772
1 parent 8c60cbf commit b4ed62d

File tree

2 files changed

+149
-4
lines changed

2 files changed

+149
-4
lines changed

packages/core/src/render3/util/signal_debug.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
SIGNAL_NODE,
2626
SignalNode,
2727
} from '../../../primitives/signals';
28+
import {isLView} from '../interfaces/type_checks';
2829

2930
export interface DebugSignalGraphNode {
3031
kind: string;
@@ -79,9 +80,10 @@ function getTemplateConsumer(injector: NodeInjector): ReactiveLViewConsumer | nu
7980
const lView = getNodeInjectorLView(injector)!;
8081
assertLView(lView);
8182
const templateLView = lView[tNode.index]!;
82-
assertLView(templateLView);
83-
84-
return templateLView[REACTIVE_TEMPLATE_CONSUMER];
83+
if (isLView(templateLView)) {
84+
return templateLView[REACTIVE_TEMPLATE_CONSUMER] ?? null;
85+
}
86+
return null;
8587
}
8688

8789
function getNodesAndEdgesFromSignalMap(signalMap: ReadonlyMap<ReactiveNode, ReactiveNode[]>): {

packages/core/test/acceptance/signal_debug_spec.ts

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

9-
import {Component, computed, effect, inject, Injectable, signal} from '../../src/core';
9+
import {NodeInjector} from '../../../core/src/render3/di';
10+
import {getDirectives} from '../../../core/src/render3/util/discovery_utils';
11+
import {
12+
Component,
13+
Directive,
14+
computed,
15+
effect,
16+
inject,
17+
Injectable,
18+
signal,
19+
Injector,
20+
} from '../../src/core';
1021
import {
1122
getFrameworkDIDebugData,
1223
setupFrameworkInjectorProfiler,
@@ -302,4 +313,136 @@ describe('getSignalGraph', () => {
302313
producer: fourFiveSixNode,
303314
});
304315
}));
316+
317+
it('should capture signals created in directives in the signal graph', () => {
318+
@Directive({
319+
selector: '[myDirective]',
320+
})
321+
class MyDirective {
322+
injector = inject(Injector);
323+
readonly fooSignal = signal('foo', {debugName: 'fooSignal'});
324+
readonly barEffect = effect(
325+
() => {
326+
this.fooSignal();
327+
},
328+
{debugName: 'barEffect'},
329+
);
330+
}
331+
332+
@Component({
333+
selector: 'component-with-directive',
334+
template: `<div id="element-with-directive" myDirective></div>`,
335+
imports: [MyDirective],
336+
})
337+
class WithDirective {}
338+
339+
TestBed.configureTestingModule({imports: [WithDirective]});
340+
const fixture = TestBed.createComponent(WithDirective);
341+
fixture.detectChanges();
342+
343+
const element = fixture.nativeElement.querySelector('#element-with-directive');
344+
// get the directive instance
345+
const directiveInstances = getDirectives(element);
346+
expect(directiveInstances.length).toBe(1);
347+
const directiveInstance = directiveInstances[0];
348+
expect(directiveInstance).toBeInstanceOf(MyDirective);
349+
const injector = (directiveInstance as MyDirective).injector;
350+
expect(injector).toBeInstanceOf(NodeInjector);
351+
const signalGraph = getSignalGraph(injector);
352+
expect(signalGraph).toBeDefined();
353+
354+
const {nodes, edges} = signalGraph;
355+
expect(nodes.length).toBe(2);
356+
expect(edges.length).toBe(1);
357+
358+
const fooNode = nodes.find((node) => node.label === 'fooSignal');
359+
expect(fooNode).toBeDefined();
360+
expect(fooNode!.value).toBe('foo');
361+
362+
const barNode = nodes.find((node) => node.label === 'barEffect');
363+
expect(barNode).toBeDefined();
364+
expect(barNode!.kind).toBe('effect');
365+
366+
const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes);
367+
expect(edgesWithNodes).toContain({consumer: barNode!, producer: fooNode!});
368+
});
369+
370+
it('should capture signals created in different directives in the signal graph', () => {
371+
@Directive({
372+
selector: '[myDirectiveA]',
373+
})
374+
class MyDirectiveA {
375+
injector = inject(Injector);
376+
readonly signalA = signal('A', {debugName: 'signalA'});
377+
readonly effectB = effect(
378+
() => {
379+
this.signalA();
380+
},
381+
{debugName: 'effectB'},
382+
);
383+
}
384+
385+
@Directive({
386+
selector: '[myDirectiveB]',
387+
})
388+
class MyDirectiveB {
389+
injector = inject(Injector);
390+
readonly signalC = signal('C', {debugName: 'signalC'});
391+
readonly effectD = effect(
392+
() => {
393+
this.signalC();
394+
},
395+
{debugName: 'effectD'},
396+
);
397+
}
398+
399+
@Component({
400+
selector: 'component-with-multiple-directives',
401+
template: `<div id="element-with-directives" myDirectiveA myDirectiveB></div>`,
402+
imports: [MyDirectiveA, MyDirectiveB],
403+
})
404+
class WithMultipleDirectives {}
405+
406+
TestBed.configureTestingModule({imports: [WithMultipleDirectives]});
407+
const fixture = TestBed.createComponent(WithMultipleDirectives);
408+
fixture.detectChanges();
409+
const element = fixture.nativeElement.querySelector('#element-with-directives');
410+
// get the directive instances
411+
const directiveInstances = getDirectives(element);
412+
expect(directiveInstances.length).toBe(2);
413+
const directiveInstanceA = directiveInstances[0];
414+
const directiveInstanceB = directiveInstances[1];
415+
expect(directiveInstanceA).toBeInstanceOf(MyDirectiveA);
416+
expect(directiveInstanceB).toBeInstanceOf(MyDirectiveB);
417+
const injector = (directiveInstanceA as MyDirectiveA).injector;
418+
expect(injector).toBeInstanceOf(NodeInjector);
419+
420+
const signalGraph = getSignalGraph(injector);
421+
expect(signalGraph).toBeDefined();
422+
const {nodes, edges} = signalGraph;
423+
expect(nodes.length).toBe(4);
424+
expect(edges.length).toBe(2);
425+
426+
const signalANode = nodes.find((node) => node.label === 'signalA');
427+
expect(signalANode).toBeDefined();
428+
expect(signalANode!.kind).toBe('signal');
429+
expect(signalANode!.value).toBe('A');
430+
431+
const effectBNode = nodes.find((node) => node.label === 'effectB');
432+
expect(effectBNode).toBeDefined();
433+
expect(effectBNode!.kind).toBe('effect');
434+
435+
const signalCNode = nodes.find((node) => node.label === 'signalC');
436+
expect(signalCNode).toBeDefined();
437+
expect(signalCNode!.kind).toBe('signal');
438+
expect(signalCNode!.value).toBe('C');
439+
440+
const effectDNode = nodes.find((node) => node.label === 'effectD');
441+
expect(effectDNode).toBeDefined();
442+
expect(effectDNode!.kind).toBe('effect');
443+
444+
const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes);
445+
expect(edgesWithNodes).toContain({consumer: effectBNode!, producer: signalANode!});
446+
expect(edgesWithNodes).toContain({consumer: effectDNode!, producer: signalCNode!});
447+
});
305448
});

0 commit comments

Comments
 (0)