@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
22import { renderHook , act } from "@testing-library/react" ;
33import { useUnicornScene } from "../shared/hooks" ;
44import type { UnicornStudioScene } from "../shared/types" ;
5+ import { MockResizeObserver } from "./setup" ;
56
67// ---------------------------------------------------------------------------
78// Helpers
@@ -62,6 +63,7 @@ describe("useUnicornScene", () => {
6263 vi . restoreAllMocks ( ) ;
6364 delete ( window as Record < string , unknown > ) . UnicornStudio ;
6465 containerEl . remove ( ) ;
66+ MockResizeObserver . instances . length = 0 ;
6567 } ) ;
6668
6769 // -----------------------------------------------------------------------
@@ -103,9 +105,7 @@ describe("useUnicornScene", () => {
103105 addSceneMock . mockResolvedValueOnce ( scene ) ;
104106 const onLoad = vi . fn ( ) ;
105107
106- renderHook ( ( ) =>
107- useUnicornScene ( { ...defaultProps ( elementRef ) , onLoad } ) ,
108- ) ;
108+ renderHook ( ( ) => useUnicornScene ( { ...defaultProps ( elementRef ) , onLoad } ) ) ;
109109
110110 await act ( async ( ) => { } ) ;
111111
@@ -135,10 +135,9 @@ describe("useUnicornScene", () => {
135135 const scene = createMockScene ( ) ;
136136 addSceneMock . mockResolvedValue ( scene ) ;
137137
138- const { rerender } = renderHook (
139- ( props ) => useUnicornScene ( props ) ,
140- { initialProps : { ...defaultProps ( elementRef ) , onLoad : ( ) => { } } } ,
141- ) ;
138+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
139+ initialProps : { ...defaultProps ( elementRef ) , onLoad : ( ) => { } } ,
140+ } ) ;
142141
143142 await act ( async ( ) => { } ) ;
144143 expect ( addSceneMock ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -160,10 +159,9 @@ describe("useUnicornScene", () => {
160159 const scene2 = createMockScene ( ) ;
161160 addSceneMock . mockResolvedValueOnce ( scene1 ) . mockResolvedValueOnce ( scene2 ) ;
162161
163- const { rerender } = renderHook (
164- ( props ) => useUnicornScene ( props ) ,
165- { initialProps : defaultProps ( elementRef ) } ,
166- ) ;
162+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
163+ initialProps : defaultProps ( elementRef ) ,
164+ } ) ;
167165
168166 await act ( async ( ) => { } ) ;
169167 expect ( addSceneMock ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -206,14 +204,16 @@ describe("useUnicornScene", () => {
206204 let resolveFirst ! : ( s : UnicornStudioScene ) => void ;
207205 addSceneMock
208206 . mockImplementationOnce (
209- ( ) => new Promise < UnicornStudioScene > ( ( r ) => { resolveFirst = r ; } ) ,
207+ ( ) =>
208+ new Promise < UnicornStudioScene > ( ( r ) => {
209+ resolveFirst = r ;
210+ } ) ,
210211 )
211212 . mockResolvedValueOnce ( createMockScene ( ) ) ;
212213
213- const { rerender } = renderHook (
214- ( props ) => useUnicornScene ( props ) ,
215- { initialProps : defaultProps ( elementRef ) } ,
216- ) ;
214+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
215+ initialProps : defaultProps ( elementRef ) ,
216+ } ) ;
217217
218218 await act ( async ( ) => { } ) ;
219219 expect ( addSceneMock ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -226,7 +226,9 @@ describe("useUnicornScene", () => {
226226
227227 // Resolve the first (its ignore flag is set, so the scene gets destroyed)
228228 const lateScene = createMockScene ( ) ;
229- await act ( async ( ) => { resolveFirst ( lateScene ) ; } ) ;
229+ await act ( async ( ) => {
230+ resolveFirst ( lateScene ) ;
231+ } ) ;
230232 expect ( lateScene . destroy ) . toHaveBeenCalled ( ) ;
231233 } ) ;
232234
@@ -279,15 +281,12 @@ describe("useUnicornScene", () => {
279281 const oldRef : { current : UnicornStudioScene | null } = { current : null } ;
280282 const newRef : { current : UnicornStudioScene | null } = { current : null } ;
281283
282- const { rerender } = renderHook (
283- ( props ) => useUnicornScene ( props ) ,
284- {
285- initialProps : {
286- ...defaultProps ( elementRef ) ,
287- sceneRef : oldRef ,
288- } ,
284+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
285+ initialProps : {
286+ ...defaultProps ( elementRef ) ,
287+ sceneRef : oldRef ,
289288 } ,
290- ) ;
289+ } ) ;
291290
292291 await act ( async ( ) => { } ) ;
293292 expect ( oldRef . current ) . toBe ( scene ) ;
@@ -307,15 +306,12 @@ describe("useUnicornScene", () => {
307306 const oldRefFn = vi . fn ( ) ;
308307 const newRefFn = vi . fn ( ) ;
309308
310- const { rerender } = renderHook (
311- ( props ) => useUnicornScene ( props ) ,
312- {
313- initialProps : {
314- ...defaultProps ( elementRef ) ,
315- sceneRef : oldRefFn ,
316- } ,
309+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
310+ initialProps : {
311+ ...defaultProps ( elementRef ) ,
312+ sceneRef : oldRefFn ,
317313 } ,
318- ) ;
314+ } ) ;
319315
320316 await act ( async ( ) => { } ) ;
321317 expect ( oldRefFn ) . toHaveBeenCalledWith ( scene ) ;
@@ -337,10 +333,9 @@ describe("useUnicornScene", () => {
337333 const scene = createMockScene ( { paused : false } ) ;
338334 addSceneMock . mockResolvedValueOnce ( scene ) ;
339335
340- const { rerender } = renderHook (
341- ( props ) => useUnicornScene ( props ) ,
342- { initialProps : { ...defaultProps ( elementRef ) , paused : false } } ,
343- ) ;
336+ const { rerender } = renderHook ( ( props ) => useUnicornScene ( props ) , {
337+ initialProps : { ...defaultProps ( elementRef ) , paused : false } ,
338+ } ) ;
344339
345340 await act ( async ( ) => { } ) ;
346341
@@ -407,7 +402,10 @@ describe("useUnicornScene", () => {
407402 const scene = createMockScene ( ) ;
408403 let resolveAddScene ! : ( s : UnicornStudioScene ) => void ;
409404 addSceneMock . mockImplementationOnce (
410- ( ) => new Promise < UnicornStudioScene > ( ( r ) => { resolveAddScene = r ; } ) ,
405+ ( ) =>
406+ new Promise < UnicornStudioScene > ( ( r ) => {
407+ resolveAddScene = r ;
408+ } ) ,
411409 ) ;
412410
413411 const { unmount } = renderHook ( ( ) =>
@@ -435,10 +433,9 @@ describe("useUnicornScene", () => {
435433 addSceneMock . mockResolvedValue ( scene ) ;
436434
437435 const props = defaultProps ( elementRef ) ;
438- const { rerender } = renderHook (
439- ( p ) => useUnicornScene ( p ) ,
440- { initialProps : props } ,
441- ) ;
436+ const { rerender } = renderHook ( ( p ) => useUnicornScene ( p ) , {
437+ initialProps : props ,
438+ } ) ;
442439
443440 await act ( async ( ) => { } ) ;
444441 expect ( addSceneMock ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -581,4 +578,114 @@ describe("useUnicornScene", () => {
581578 expect ( onError ) . toHaveBeenCalled ( ) ;
582579 expect ( result . current . error ) . toBeInstanceOf ( Error ) ;
583580 } ) ;
581+
582+ // -----------------------------------------------------------------------
583+ // ResizeObserver – container resize triggers scene.resize()
584+ // -----------------------------------------------------------------------
585+
586+ it ( "calls scene.resize() when the container is resized" , async ( ) => {
587+ const resizeFn = vi . fn ( ) ;
588+ const scene = createMockScene ( { resize : resizeFn } ) ;
589+ addSceneMock . mockResolvedValueOnce ( scene ) ;
590+
591+ renderHook ( ( ) => useUnicornScene ( defaultProps ( elementRef ) ) ) ;
592+
593+ await act ( async ( ) => { } ) ;
594+
595+ // Find the observer watching our container
596+ const observer = MockResizeObserver . instances . find ( ( o ) =>
597+ o . elements . has ( containerEl ) ,
598+ ) ;
599+ expect ( observer ) . toBeDefined ( ) ;
600+
601+ // Simulate a container resize
602+ act ( ( ) => {
603+ observer ! . simulateResize ( containerEl ) ;
604+ } ) ;
605+
606+ expect ( resizeFn ) . toHaveBeenCalledTimes ( 1 ) ;
607+ } ) ;
608+
609+ it ( "handles scene.resize being undefined gracefully" , async ( ) => {
610+ const scene = createMockScene ( { resize : undefined } ) ;
611+ addSceneMock . mockResolvedValueOnce ( scene ) ;
612+
613+ renderHook ( ( ) => useUnicornScene ( defaultProps ( elementRef ) ) ) ;
614+
615+ await act ( async ( ) => { } ) ;
616+
617+ const observer = MockResizeObserver . instances . find ( ( o ) =>
618+ o . elements . has ( containerEl ) ,
619+ ) ;
620+
621+ // Should not throw when resize is undefined
622+ expect ( ( ) => {
623+ act ( ( ) => {
624+ observer ! . simulateResize ( containerEl ) ;
625+ } ) ;
626+ } ) . not . toThrow ( ) ;
627+ } ) ;
628+
629+ it ( "disconnects the ResizeObserver on unmount" , async ( ) => {
630+ const scene = createMockScene ( { resize : vi . fn ( ) } ) ;
631+ addSceneMock . mockResolvedValueOnce ( scene ) ;
632+
633+ const { unmount } = renderHook ( ( ) =>
634+ useUnicornScene ( defaultProps ( elementRef ) ) ,
635+ ) ;
636+
637+ await act ( async ( ) => { } ) ;
638+
639+ const observer = MockResizeObserver . instances . find ( ( o ) =>
640+ o . elements . has ( containerEl ) ,
641+ ) ;
642+ expect ( observer ) . toBeDefined ( ) ;
643+
644+ unmount ( ) ;
645+
646+ expect ( observer ! . disconnect ) . toHaveBeenCalled ( ) ;
647+ } ) ;
648+
649+ it ( "degrades gracefully when ResizeObserver is unavailable" , async ( ) => {
650+ const originalResizeObserver = globalThis . ResizeObserver ;
651+ // @ts -expect-error -- simulating an environment without ResizeObserver
652+ delete globalThis . ResizeObserver ;
653+
654+ try {
655+ addSceneMock . mockResolvedValueOnce ( createMockScene ( { resize : vi . fn ( ) } ) ) ;
656+
657+ const { unmount } = renderHook ( ( ) =>
658+ useUnicornScene ( defaultProps ( elementRef ) ) ,
659+ ) ;
660+
661+ await act ( async ( ) => { } ) ;
662+
663+ // No ResizeObserver should have been created
664+ expect ( MockResizeObserver . instances ) . toHaveLength ( 0 ) ;
665+
666+ // Should not throw on unmount either
667+ expect ( ( ) => unmount ( ) ) . not . toThrow ( ) ;
668+ } finally {
669+ globalThis . ResizeObserver = originalResizeObserver ;
670+ }
671+ } ) ;
672+
673+ it ( "does not create a ResizeObserver when elementRef is null" , async ( ) => {
674+ const instancesBefore = MockResizeObserver . instances . length ;
675+
676+ renderHook ( ( ) =>
677+ useUnicornScene ( {
678+ ...defaultProps ( { current : null } ) ,
679+ isScriptLoaded : true ,
680+ } ) ,
681+ ) ;
682+
683+ await act ( async ( ) => { } ) ;
684+
685+ const newObservers = MockResizeObserver . instances . slice ( instancesBefore ) ;
686+ const observersWithElements = newObservers . filter (
687+ ( o ) => o . elements . size > 0 ,
688+ ) ;
689+ expect ( observersWithElements ) . toHaveLength ( 0 ) ;
690+ } ) ;
584691} ) ;
0 commit comments