Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(platform-browser): avoid SharedStylesHost modifications af…
…ter destruction

Since `SharedStylesHost` can receive `addHost` and `removeHost` calls after it is destroyed in testing environments (e.g., when fakeAsync triggers CD after teardown), we need to safely no-op these operations to avoid errors.
  • Loading branch information
dgp1130 committed Mar 28, 2026
commit 1ccb813c45922d14b81c4d192d525c40ff84291b
16 changes: 16 additions & 0 deletions packages/platform-browser/src/dom/shared_styles_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
*/
private readonly hosts = new Map<Node, number>();

/** Whether this instance has been destroyed. */
private destroyed = false;

constructor(
@Inject(DOCUMENT) private readonly doc: Document,
@Inject(APP_ID) private readonly appId: string,
Expand Down Expand Up @@ -202,9 +205,16 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
removeElements(elements);
}
this.hosts.clear();
this.destroyed = true;
}

addHost(hostNode: Node): void {
// Adding a host after destruction will have no effect and is likely a bug in the caller.
// However, some testing scenarios with fake async appear to trigger CD after `TestBed`
// teardown, meaning Angular may render new components (and then immediately destroy them)
// after the application is destroyed, so we have to allow this to happen and no-op.
if (this.destroyed) return;

const existingUsage = this.hosts.get(hostNode) ?? 0;
if (existingUsage === 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking: Can we move the this.hosts.set(hostNode, existingUsage + 1); up and invert this guard to reduce nesting?

// Add existing styles to new host
Expand All @@ -220,6 +230,12 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
}

removeHost(hostNode: Node): void {
// In some scenarios (such as an explicit `ApplicationRef.prototype.destroy` call),
// this instance's `ngOnDestroy` method may be called before a component is destroyed and
// attempts to remove its own styles. In this case, we need to no-op to avoid throwing an
// error.
if (this.destroyed) return;

const usage = this.hosts.get(hostNode);
if (typeof ngDevMode !== 'undefined' && ngDevMode && usage === undefined) {
Comment thread
dgp1130 marked this conversation as resolved.
throw new Error('Attempted to remove a host which was not added.');
Expand Down
17 changes: 17 additions & 0 deletions packages/platform-browser/test/dom/shared_styles_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,21 @@ describe('SharedStylesHost', () => {
expect(doc.head.innerHTML).not.toContain('ng-app-id');
});
});

describe('destroy', () => {
it('should ignore `addHost` and `removeHost` calls after destruction', () => {
ssh.addStyles(['a {};']);
ssh.addHost(someHost);
expect(someHost.innerHTML).toEqual('<style>a {};</style>');

ssh.ngOnDestroy();
expect(someHost.innerHTML).toEqual('');

// These should be no-ops instead of crashing or modifying things.
expect(() => void ssh.addHost(someHost)).not.toThrow();
expect(someHost.innerHTML).toEqual('');
expect(() => void ssh.removeHost(someHost)).not.toThrow();
expect(someHost.innerHTML).toEqual('');
});
});
});