@@ -9,6 +9,12 @@ import inlineHTML from "./Inline/inlineHtml";
99import { EMBED_DARK_THEME_CLASS , EMBED_LIGHT_THEME_CLASS } from "./constants" ;
1010import { getColorSchemeDarkQuery } from "./ui-utils" ;
1111
12+ type EmbedElementWithPrivateMethodsAccess = {
13+ boundResizeHandler : ( ) => void ;
14+ boundPrefersDarkThemeChangedHandler : ( e : MediaQueryListEvent ) => void ;
15+ boundEnsureContainerTakesSkeletonHeightWhenVisible : ( ) => void ;
16+ }
17+
1218( function defineEmbedTestElement ( ) {
1319 class TestEmbedElement extends EmbedElement {
1420 constructor ( {
@@ -31,6 +37,75 @@ import { getColorSchemeDarkQuery } from "./ui-utils";
3137 customElements . define ( "test-embed" , TestEmbedElement ) ;
3238} ) ( ) ;
3339
40+ function mockWindowEventListeners ( ) {
41+ const eventListenerCallbacks = new Map < string , EventListenerOrEventListenerObject > ( ) ;
42+ const animationFrameCallbacks : Map < number , FrameRequestCallback > = new Map ( ) ;
43+ const colorSchemeListenerCallbacks : Map < string , EventListenerOrEventListenerObject > = new Map ( ) ;
44+ const colorSchemeQuery = getColorSchemeDarkQuery ( ) ;
45+ let nextAnimationFrameId = 1 ;
46+
47+ vi
48+ . spyOn ( window , "addEventListener" )
49+ . mockImplementation ( ( event : string , callback : EventListenerOrEventListenerObject ) => {
50+ eventListenerCallbacks . set ( event , callback ) ;
51+ } ) ;
52+
53+ vi
54+ . spyOn ( window , "removeEventListener" )
55+ . mockImplementation ( ( event : string , callback : EventListenerOrEventListenerObject ) => {
56+ const registeredCallback = eventListenerCallbacks . get ( event ) ;
57+ expect ( registeredCallback ) . toBe ( callback ) ;
58+ eventListenerCallbacks . delete ( event ) ;
59+ } ) ;
60+
61+ vi
62+ . spyOn ( window , "requestAnimationFrame" )
63+ . mockImplementation ( ( callback : FrameRequestCallback ) => {
64+ const id = nextAnimationFrameId ++ ;
65+ animationFrameCallbacks . set ( id , callback ) ;
66+ return id ;
67+ } ) ;
68+
69+ vi
70+ . spyOn ( window , "cancelAnimationFrame" )
71+ . mockImplementation ( ( id : number ) => {
72+ animationFrameCallbacks . delete ( id ) ;
73+ } ) ;
74+
75+ vi
76+ . spyOn ( colorSchemeQuery , "addEventListener" )
77+ . mockImplementation ( ( event : string , callback : EventListenerOrEventListenerObject ) => {
78+ colorSchemeListenerCallbacks . set ( event , callback ) ;
79+ } ) ;
80+
81+ vi
82+ . spyOn ( colorSchemeQuery , "removeEventListener" )
83+ . mockImplementation ( ( event : string , callback : EventListenerOrEventListenerObject ) => {
84+ colorSchemeListenerCallbacks . delete ( event ) ;
85+ } ) ;
86+
87+ return {
88+ expectListenerToBeRegistered : ( event : string , callback : EventListenerOrEventListenerObject ) => {
89+ expect ( eventListenerCallbacks . get ( event ) ) . toBe ( callback ) ;
90+ } ,
91+ expectListenerToBeUnregistered : ( event : string , callback : EventListenerOrEventListenerObject ) => {
92+ expect ( eventListenerCallbacks . get ( event ) ) . not . toBe ( callback ) ;
93+ } ,
94+ expectAnimationFrameListenerToBeRegistered : ( rafId : number , callback : FrameRequestCallback ) => {
95+ expect ( animationFrameCallbacks . get ( rafId ) ) . toBe ( callback ) ;
96+ } ,
97+ expectAnimationFrameListenerToBeUnregistered : ( rafId : number , callback : FrameRequestCallback ) => {
98+ expect ( animationFrameCallbacks . get ( rafId ) ) . not . toBe ( callback ) ;
99+ } ,
100+ expectColorSchemeListenerToBeRegistered : ( callback : ( e : MediaQueryListEvent ) => void ) => {
101+ expect ( colorSchemeListenerCallbacks . get ( "change" ) ) . toBe ( callback ) ;
102+ } ,
103+ expectColorSchemeListenerToBeUnregistered : ( callback : ( e : MediaQueryListEvent ) => void ) => {
104+ expect ( colorSchemeListenerCallbacks . get ( "change" ) ) . not . toBe ( callback ) ;
105+ } ,
106+ } ;
107+ }
108+
34109function buildMediaQueryListEvent ( { type, matches } : { type : string ; matches : boolean } ) {
35110 return {
36111 type,
@@ -143,7 +218,9 @@ describe("EmbedElement", () => {
143218 if ( ! element ) {
144219 throw new Error ( "`element` not defined" ) ;
145220 }
146- document . body . removeChild ( element ) ;
221+ if ( element . parentNode ) {
222+ document . body . removeChild ( element ) ;
223+ }
147224 vi . restoreAllMocks ( ) ;
148225 } ) ;
149226
@@ -358,5 +435,33 @@ describe("EmbedElement", () => {
358435 expectLayoutToBe ( "month_view" , element ) ;
359436 } ) ;
360437 } ) ;
438+
439+ describe ( "Cleanup Behavior" , ( ) => {
440+ it ( "should clean up all resources when element is disconnected" , ( ) => {
441+ const { expectListenerToBeRegistered, expectListenerToBeUnregistered, expectAnimationFrameListenerToBeRegistered, expectAnimationFrameListenerToBeUnregistered, expectColorSchemeListenerToBeRegistered, expectColorSchemeListenerToBeUnregistered } = mockWindowEventListeners ( ) ;
442+
443+ element = createTestEmbedElement ( {
444+ dataset : { pageType : "user.event.booking.slots" } ,
445+ } ) ;
446+
447+
448+
449+ const internalEmbed = element as unknown as EmbedElementWithPrivateMethodsAccess ;
450+
451+ const boundResizeHandler = internalEmbed . boundResizeHandler ;
452+ const boundPrefersDarkThemeChangedHandler = internalEmbed . boundPrefersDarkThemeChangedHandler ;
453+ const boundEnsureContainerTakesSkeletonHeightWhenVisible = internalEmbed . boundEnsureContainerTakesSkeletonHeightWhenVisible ;
454+
455+ expectListenerToBeRegistered ( "resize" , boundResizeHandler ) ;
456+ expectColorSchemeListenerToBeRegistered ( boundPrefersDarkThemeChangedHandler ) ;
457+ expectAnimationFrameListenerToBeRegistered ( element . skeletonContainerHeightTimer ! , boundEnsureContainerTakesSkeletonHeightWhenVisible ) ;
458+
459+ document . body . removeChild ( element ) ;
460+
461+ expectListenerToBeUnregistered ( "resize" , boundResizeHandler ) ;
462+ expectColorSchemeListenerToBeUnregistered ( boundPrefersDarkThemeChangedHandler ) ;
463+ expectAnimationFrameListenerToBeUnregistered ( element . skeletonContainerHeightTimer ! , boundEnsureContainerTakesSkeletonHeightWhenVisible ) ;
464+ } ) ;
465+ } ) ;
361466 } ) ;
362467} ) ;
0 commit comments