mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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
This commit is contained in:
parent
2bd3a43028
commit
9a8f4f14aa
2 changed files with 149 additions and 4 deletions
|
|
@ -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<ReactiveNode, string>();
|
||||
|
|
|
|||
|
|
@ -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: `<div id="element-with-directive" myDirective></div>`,
|
||||
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: `<div id="element-with-directives" myDirectiveA myDirectiveB></div>`,
|
||||
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!});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue