@@ -121,6 +121,9 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
121121 */
122122 private readonly hosts = new Map < Node , number > ( ) ;
123123
124+ /** Whether this instance has been destroyed. */
125+ private destroyed = false ;
126+
124127 constructor (
125128 @Inject ( DOCUMENT ) private readonly doc : Document ,
126129 @Inject ( APP_ID ) private readonly appId : string ,
@@ -202,9 +205,16 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
202205 removeElements ( elements ) ;
203206 }
204207 this . hosts . clear ( ) ;
208+ this . destroyed = true ;
205209 }
206210
207211 addHost ( hostNode : Node ) : void {
212+ // Adding a host after destruction will have no effect and is likely a bug in the caller.
213+ // However, some testing scenarios with fake async appear to trigger CD after `TestBed`
214+ // teardown, meaning Angular may render new components (and then immediately destroy them)
215+ // after the application is destroyed, so we have to allow this to happen and no-op.
216+ if ( this . destroyed ) return ;
217+
208218 const existingUsage = this . hosts . get ( hostNode ) ?? 0 ;
209219 if ( existingUsage === 0 ) {
210220 // Add existing styles to new host
@@ -220,6 +230,12 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
220230 }
221231
222232 removeHost ( hostNode : Node ) : void {
233+ // In some scenarios (such as an explicit `ApplicationRef.prototype.destroy` call),
234+ // this instance's `ngOnDestroy` method may be called before a component is destroyed and
235+ // attempts to remove its own styles. In this case, we need to no-op to avoid throwing an
236+ // error.
237+ if ( this . destroyed ) return ;
238+
223239 const usage = this . hosts . get ( hostNode ) ;
224240 if ( typeof ngDevMode !== 'undefined' && ngDevMode && usage === undefined ) {
225241 throw new Error ( 'Attempted to remove a host which was not added.' ) ;
0 commit comments