@@ -23,6 +23,7 @@ import {
2323 provideZoneChangeDetection ,
2424 QueryList ,
2525 SimpleChanges ,
26+ StyleRoot ,
2627 Type ,
2728 ViewChild ,
2829 ViewChildren ,
@@ -39,14 +40,18 @@ import {clearTranslations, loadTranslations} from '@angular/localize';
3940import { computeMsgId } from '@angular/compiler' ;
4041import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser' ;
4142import { ComponentType } from '../../src/render3' ;
43+ import { ɵSharedStylesHost as SharedStylesHost } from '@angular/platform-browser' ;
4244import { isNode } from '@angular/private/testing' ;
4345
4446describe ( 'hot module replacement' , ( ) => {
4547 beforeEach ( ( ) => {
4648 TestBed . configureTestingModule ( {
4749 providers : [ provideZoneChangeDetection ( ) ] ,
4850 } ) ;
51+
52+ for ( const child of document . body . childNodes ) child . remove ( ) ;
4953 } ) ;
54+
5055 it ( 'should recreate a single usage of a basic component' , ( ) => {
5156 let instance ! : ChildCmp ;
5257 const initialMetadata : Component = {
@@ -285,11 +290,11 @@ describe('hot module replacement', () => {
285290 const getShadowRoot = ( ) => fixture . nativeElement . querySelector ( 'child-cmp' ) . shadowRoot ;
286291
287292 markNodesAsCreatedInitially ( getShadowRoot ( ) ) ;
288- expectHTML ( getShadowRoot ( ) , `< style>strong {color: red;}</style>Hello <strong>0</strong >` ) ;
293+ expectHTML ( getShadowRoot ( ) , `Hello <strong>0</strong>< style>strong {color: red;}</style>` ) ;
289294
290295 instance . state = 1 ;
291296 fixture . detectChanges ( ) ;
292- expectHTML ( getShadowRoot ( ) , `< style>strong {color: red;}</style>Hello <strong>1</strong >` ) ;
297+ expectHTML ( getShadowRoot ( ) , `Hello <strong>1</strong>< style>strong {color: red;}</style>` ) ;
293298
294299 replaceMetadata ( ChildCmp , {
295300 ...initialMetadata ,
@@ -307,6 +312,103 @@ describe('hot module replacement', () => {
307312 getShadowRoot ( ) ,
308313 `<style>strong {background: pink;}</style>Changed <strong>1</strong>!` ,
309314 ) ;
315+
316+ fixture . destroy ( ) ;
317+ assertNoLeakedStyles ( TestBed . inject ( SharedStylesHost ) ) ;
318+ } ) ;
319+
320+ // TODO: `NoneEncapsulationDomRenderer.prototype.removeStyles` skips when animations are running, breaks in full test suite in headless mode.
321+ // Don't *think* it's related to rAF throttling.
322+ it ( "should replace a component child's styles within shadow DOM encapsulation" , async ( ) => {
323+ // Domino doesn't support shadow DOM.
324+ if ( isNode ) {
325+ return ;
326+ }
327+
328+ const initialMetadata : Component = {
329+ selector : 'child-cmp' ,
330+ template : 'Hello <strong>World</strong>!' ,
331+ styles : `strong {color: red;}` ,
332+ encapsulation : ViewEncapsulation . None ,
333+ } ;
334+
335+ @Component ( initialMetadata )
336+ class ChildCmp { }
337+
338+ @Component ( {
339+ template : '<child-cmp/>' ,
340+ encapsulation : ViewEncapsulation . ShadowDom ,
341+ imports : [ ChildCmp ] ,
342+ } )
343+ class RootCmp { }
344+
345+ const fixture = TestBed . createComponent ( RootCmp ) ;
346+ fixture . detectChanges ( ) ;
347+ const getShadowRoot = ( ) => fixture . nativeElement . shadowRoot ;
348+
349+ expect ( getShadowRoot ( ) . innerHTML ) . toContain ( `<style>strong {color: red;}</style>` ) ;
350+
351+ replaceMetadata ( ChildCmp , {
352+ ...initialMetadata ,
353+ styles : `strong {background: pink;}` ,
354+ } ) ;
355+ fixture . detectChanges ( ) ;
356+
357+ expect ( getShadowRoot ( ) . innerHTML ) . toContain ( '<style>strong {background: pink;}</style>' ) ;
358+ expect ( getShadowRoot ( ) . innerHTML ) . not . toContain ( `<style>strong {color: red;}</style>` ) ;
359+
360+ fixture . destroy ( ) ;
361+ assertNoLeakedStyles ( TestBed . inject ( SharedStylesHost ) ) ;
362+ } ) ;
363+
364+ it ( 'should support components within nested shadow DOM' , ( ) => {
365+ // Domino doesn't support shadow DOM.
366+ if ( isNode ) {
367+ return ;
368+ }
369+
370+ @Component ( {
371+ selector : 'child-cmp' ,
372+ template : 'Hello <strong>{{state}}</strong>' ,
373+ styles : `
374+ strong {
375+ color: red;
376+ }
377+ ` ,
378+ encapsulation : ViewEncapsulation . ShadowDom ,
379+ } )
380+ class ChildCmp { }
381+
382+ const initialMetadata : Component = {
383+ template : '<child-cmp />' ,
384+ styles : `:host {color: red;}` ,
385+ encapsulation : ViewEncapsulation . ShadowDom ,
386+ imports : [ ChildCmp ] ,
387+ } ;
388+
389+ @Component ( initialMetadata )
390+ class RootCmp { }
391+
392+ const fixture = TestBed . createComponent ( RootCmp ) ;
393+ fixture . detectChanges ( ) ;
394+
395+ // Can't use `fixture.nativeElement` because the host element is recreated
396+ // during HMR and `fixture` is not updated.
397+ const getShadowRoot = ( ) => document . querySelector ( '[ng-version]' ) ! . shadowRoot ! ;
398+
399+ expect ( getShadowRoot ( ) . innerHTML ) . toContain ( `<style>:host {color: red;}</style>` ) ;
400+
401+ replaceMetadata ( RootCmp , {
402+ ...initialMetadata ,
403+ styles : `:host {background: pink;}` ,
404+ } ) ;
405+ fixture . detectChanges ( ) ;
406+
407+ expect ( getShadowRoot ( ) . innerHTML ) . toContain ( '<style>:host {background: pink;}</style>' ) ;
408+ expect ( getShadowRoot ( ) . innerHTML ) . not . toContain ( `<style>:host {color: red;}</style>` ) ;
409+
410+ fixture . destroy ( ) ;
411+ assertNoLeakedStyles ( TestBed . inject ( SharedStylesHost ) ) ;
310412 } ) ;
311413
312414 it ( 'should continue binding inputs to a component that is replaced' , ( ) => {
@@ -2173,4 +2275,18 @@ describe('hot module replacement', () => {
21732275 }
21742276 }
21752277 }
2278+
2279+ function assertNoLeakedStyles ( sharedStylesHost : SharedStylesHost ) : void {
2280+ const ssh = sharedStylesHost as unknown as Omit < SharedStylesHost , 'inline' | 'external' > & {
2281+ inline : Map < StyleRoot , unknown > ;
2282+ external : Map < StyleRoot , unknown > ;
2283+ } ;
2284+
2285+ const totalStyles = ssh . inline . size + ssh . external . size ;
2286+ if ( totalStyles > 0 ) {
2287+ throw new Error (
2288+ `Expected \`SharedStylesHost\` to have no leaked styles, found: ${ totalStyles } .` ,
2289+ ) ;
2290+ }
2291+ }
21762292} ) ;
0 commit comments