@@ -17,6 +17,7 @@ import {
1717 PendingTasks ,
1818 PLATFORM_ID ,
1919 ɵgetDocument as getDocument ,
20+ ɵEVENT_REPLAY_QUEUE as EVENT_REPLAY_QUEUE ,
2021} from '@angular/core' ;
2122import { isPlatformBrowser } from '@angular/common' ;
2223import {
@@ -697,4 +698,149 @@ describe('event replay', () => {
697698 ) ;
698699 } ) ;
699700 } ) ;
701+
702+ describe ( 'event replay queue' , ( ) => {
703+ it ( 'should be empty on init' , async ( ) => {
704+ @Component ( {
705+ selector : 'app' ,
706+ template : '<input (click)="onClick()" />' ,
707+ } )
708+ class SimpleComponent {
709+ onClick ( ) { }
710+ }
711+
712+ const hydrationFeatures = ( ) => [ withEventReplay ( ) ] ;
713+ const html = await ssr ( SimpleComponent , { hydrationFeatures} ) ;
714+ const ssrContents = getAppContents ( html ) ;
715+ const doc = getDocument ( ) ;
716+ prepareEnvironment ( doc , ssrContents ) ;
717+ resetTViewsFor ( SimpleComponent ) ;
718+ const appRef = await hydrate ( doc , SimpleComponent , { hydrationFeatures} ) ;
719+ const queue = appRef . injector . get ( EVENT_REPLAY_QUEUE ) ;
720+ expect ( queue . length ) . toBe ( 0 ) ;
721+ } ) ;
722+
723+ it ( 'should be different for different apps' , async ( ) => {
724+ @Component ( {
725+ selector : 'app' ,
726+ template : '<input (click)="onClick()" />' ,
727+ } )
728+ class SimpleComponent {
729+ onClick ( ) { }
730+ }
731+
732+ const hydrationFeatures = ( ) => [ withEventReplay ( ) ] ;
733+ const html = await ssr ( SimpleComponent , { hydrationFeatures} ) ;
734+ const ssrContents = getAppContents ( html ) ;
735+ const doc = getDocument ( ) ;
736+ prepareEnvironment ( doc , ssrContents ) ;
737+ resetTViewsFor ( SimpleComponent ) ;
738+
739+ const appRef1 = await hydrate ( doc , SimpleComponent , { hydrationFeatures} ) ;
740+ const queue1 = appRef1 . injector . get ( EVENT_REPLAY_QUEUE ) ;
741+
742+ const appRef2 = await hydrate ( doc , SimpleComponent , { hydrationFeatures} ) ;
743+ const queue2 = appRef2 . injector . get ( EVENT_REPLAY_QUEUE ) ;
744+
745+ expect ( queue1 ) . not . toBe ( queue2 ) ;
746+ } ) ;
747+
748+ it ( 'should clear the queue after events are replayed' , async ( ) => {
749+ @Component ( {
750+ selector : 'app' ,
751+ template : `
752+ @defer (on interaction(trigger)) {
753+ <div id="content" (click)="onClick()"></div>
754+ } @placeholder {
755+ <button id="trigger">Trigger</button>
756+ }
757+ ` ,
758+ } )
759+ class SimpleComponent {
760+ onClick ( ) { }
761+ }
762+
763+ const hydrationFeatures = ( ) => [ withEventReplay ( ) ] ;
764+ const html = await ssr ( SimpleComponent , { hydrationFeatures} ) ;
765+ const ssrContents = getAppContents ( html ) ;
766+ const doc = getDocument ( ) ;
767+ prepareEnvironment ( doc , ssrContents ) ;
768+ resetTViewsFor ( SimpleComponent ) ;
769+
770+ const appRef = await hydrate ( doc , SimpleComponent , { hydrationFeatures} ) ;
771+ const queue = appRef . injector . get ( EVENT_REPLAY_QUEUE ) ;
772+ const trigger = doc . getElementById ( 'trigger' ) ! ;
773+ // This should queue the event
774+ trigger . click ( ) ;
775+
776+ // Wait for hydration to complete
777+ await appRef . whenStable ( ) ;
778+
779+ // The queue should be cleared after replay/hydration cycle completion
780+ // Note: We might need to wait for idle/microtasks if the replay is async.
781+ // But verify expectation:
782+ // The current implementation requeues if not hydrated.
783+ // But here we expect it to hydrate.
784+
785+ // For this test to trigger replay we need to ensure the block hydrates.
786+ // interaction(trigger) hydrates on click.
787+
788+ // Check that queue is handled.
789+ // queue size initially should be 0.
790+ // After click, it might briefly be 1 if we inspect synchronously?
791+ // but `triggerHydrationFromBlockName` is called.
792+ // Eventually it should be empty again.
793+ // Since `invokeRegisteredReplayListeners` triggers hydration directly and pushes to queue.
794+
795+ // We can inspect the queue if we want.
796+ // But mainly we want to ensure no crash and cleanup happens.
797+
798+ // wait for replay
799+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
800+ expect ( queue . length ) . toBe ( 0 ) ;
801+ } ) ;
802+
803+ it ( 'should release event queue references on app destroy' , async ( ) => {
804+ let appRef : any ;
805+ const appId = 'app-id-for-memory-test' ;
806+ {
807+ @Component ( {
808+ selector : 'app' ,
809+ template : '<input (click)="onClick()" />' ,
810+ } )
811+ class SimpleComponent {
812+ onClick ( ) { }
813+ }
814+
815+ const providers = [ { provide : APP_ID , useValue : appId } ] ;
816+ const hydrationFeatures = ( ) => [ withEventReplay ( ) ] ;
817+ const html = await ssr ( SimpleComponent , {
818+ hydrationFeatures,
819+ envProviders : providers ,
820+ } ) ;
821+ const ssrContents = getAppContents ( html ) ;
822+ const doc = getDocument ( ) ;
823+ prepareEnvironment ( doc , ssrContents ) ;
824+ resetTViewsFor ( SimpleComponent ) ;
825+
826+ appRef = await hydrate ( doc , SimpleComponent , {
827+ hydrationFeatures,
828+ envProviders : providers ,
829+ } ) ;
830+
831+ // Access queue to make sure it exists
832+ const queue = appRef . injector . get ( EVENT_REPLAY_QUEUE ) ;
833+ expect ( queue ) . toBeInstanceOf ( Array ) ;
834+
835+ // Simulate event in queue
836+ queue . push ( { event : new Event ( 'click' ) , currentTarget : doc . createElement ( 'div' ) } ) ;
837+ expect ( queue . length ) . toBe ( 1 ) ;
838+
839+ appRef . destroy ( ) ;
840+ }
841+
842+ // Verify global cleanup
843+ expect ( window . _ejsas ! [ appId ] ) . toBeUndefined ( ) ;
844+ } ) ;
845+ } ) ;
700846} ) ;
0 commit comments