diff --git a/packages/core/src/render3/util/signal_debug.ts b/packages/core/src/render3/util/signal_debug.ts index 4bca8b5ba19..953232fa914 100644 --- a/packages/core/src/render3/util/signal_debug.ts +++ b/packages/core/src/render3/util/signal_debug.ts @@ -25,6 +25,7 @@ import { SIGNAL_NODE, SignalNode, } from '../../../primitives/signals'; +import {isLView} from '../interfaces/type_checks'; export interface DebugSignalGraphNode { kind: string; @@ -82,9 +83,10 @@ function getTemplateConsumer(injector: NodeInjector): ReactiveLViewConsumer | nu const lView = getNodeInjectorLView(injector)!; assertLView(lView); const templateLView = lView[tNode.index]!; - assertLView(templateLView); - - return templateLView[REACTIVE_TEMPLATE_CONSUMER]; + if (isLView(templateLView)) { + return templateLView[REACTIVE_TEMPLATE_CONSUMER] ?? null; + } + return null; } const signalDebugMap = new WeakMap(); diff --git a/packages/core/test/acceptance/signal_debug_spec.ts b/packages/core/test/acceptance/signal_debug_spec.ts index af4dd6f324a..99df0a02838 100644 --- a/packages/core/test/acceptance/signal_debug_spec.ts +++ b/packages/core/test/acceptance/signal_debug_spec.ts @@ -6,7 +6,18 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Component, computed, effect, inject, Injectable, signal} from '../../src/core'; +import {NodeInjector} from '../../../core/src/render3/di'; +import {getDirectives} from '../../../core/src/render3/util/discovery_utils'; +import { + Component, + Directive, + computed, + effect, + inject, + Injectable, + signal, + Injector, +} from '../../src/core'; import { getFrameworkDIDebugData, setupFrameworkInjectorProfiler, @@ -302,4 +313,136 @@ describe('getSignalGraph', () => { producer: fourFiveSixNode, }); })); + + it('should capture signals created in directives in the signal graph', () => { + @Directive({ + selector: '[myDirective]', + }) + class MyDirective { + injector = inject(Injector); + readonly fooSignal = signal('foo', {debugName: 'fooSignal'}); + readonly barEffect = effect( + () => { + this.fooSignal(); + }, + {debugName: 'barEffect'}, + ); + } + + @Component({ + selector: 'component-with-directive', + template: `
`, + imports: [MyDirective], + }) + class WithDirective {} + + TestBed.configureTestingModule({imports: [WithDirective]}); + const fixture = TestBed.createComponent(WithDirective); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('#element-with-directive'); + // get the directive instance + const directiveInstances = getDirectives(element); + expect(directiveInstances.length).toBe(1); + const directiveInstance = directiveInstances[0]; + expect(directiveInstance).toBeInstanceOf(MyDirective); + const injector = (directiveInstance as MyDirective).injector; + expect(injector).toBeInstanceOf(NodeInjector); + const signalGraph = getSignalGraph(injector); + expect(signalGraph).toBeDefined(); + + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(2); + expect(edges.length).toBe(1); + + const fooNode = nodes.find((node) => node.label === 'fooSignal'); + expect(fooNode).toBeDefined(); + expect(fooNode!.value).toBe('foo'); + + const barNode = nodes.find((node) => node.label === 'barEffect'); + expect(barNode).toBeDefined(); + expect(barNode!.kind).toBe('effect'); + + const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes); + expect(edgesWithNodes).toContain({consumer: barNode!, producer: fooNode!}); + }); + + it('should capture signals created in different directives in the signal graph', () => { + @Directive({ + selector: '[myDirectiveA]', + }) + class MyDirectiveA { + injector = inject(Injector); + readonly signalA = signal('A', {debugName: 'signalA'}); + readonly effectB = effect( + () => { + this.signalA(); + }, + {debugName: 'effectB'}, + ); + } + + @Directive({ + selector: '[myDirectiveB]', + }) + class MyDirectiveB { + injector = inject(Injector); + readonly signalC = signal('C', {debugName: 'signalC'}); + readonly effectD = effect( + () => { + this.signalC(); + }, + {debugName: 'effectD'}, + ); + } + + @Component({ + selector: 'component-with-multiple-directives', + template: `
`, + imports: [MyDirectiveA, MyDirectiveB], + }) + class WithMultipleDirectives {} + + TestBed.configureTestingModule({imports: [WithMultipleDirectives]}); + const fixture = TestBed.createComponent(WithMultipleDirectives); + fixture.detectChanges(); + const element = fixture.nativeElement.querySelector('#element-with-directives'); + // get the directive instances + const directiveInstances = getDirectives(element); + expect(directiveInstances.length).toBe(2); + const directiveInstanceA = directiveInstances[0]; + const directiveInstanceB = directiveInstances[1]; + expect(directiveInstanceA).toBeInstanceOf(MyDirectiveA); + expect(directiveInstanceB).toBeInstanceOf(MyDirectiveB); + const injector = (directiveInstanceA as MyDirectiveA).injector; + expect(injector).toBeInstanceOf(NodeInjector); + + const signalGraph = getSignalGraph(injector); + expect(signalGraph).toBeDefined(); + const {nodes, edges} = signalGraph; + expect(nodes.length).toBe(4); + expect(edges.length).toBe(2); + + const signalANode = nodes.find((node) => node.label === 'signalA'); + expect(signalANode).toBeDefined(); + expect(signalANode!.kind).toBe('signal'); + expect(signalANode!.value).toBe('A'); + + const effectBNode = nodes.find((node) => node.label === 'effectB'); + expect(effectBNode).toBeDefined(); + expect(effectBNode!.kind).toBe('effect'); + + const signalCNode = nodes.find((node) => node.label === 'signalC'); + expect(signalCNode).toBeDefined(); + expect(signalCNode!.kind).toBe('signal'); + expect(signalCNode!.value).toBe('C'); + + const effectDNode = nodes.find((node) => node.label === 'effectD'); + expect(effectDNode).toBeDefined(); + expect(effectDNode!.kind).toBe('effect'); + + const edgesWithNodes = mapEdgeIndicesIntoNodes(edges, nodes); + expect(edgesWithNodes).toContain({consumer: effectBNode!, producer: signalANode!}); + expect(edgesWithNodes).toContain({consumer: effectDNode!, producer: signalCNode!}); + }); });