@@ -26,28 +26,28 @@ if (typeof global.BroadcastChannel === "undefined") {
2626name :string ;
2727onmessage :( ( event :MessageEvent ) => void ) | null = null ;
2828onmessageerror :( ( event :MessageEvent ) => void ) | null = null ;
29-
29+
3030constructor ( name :string ) {
3131this . name = name ;
3232}
33-
33+
3434postMessage ( _message :unknown ) {
3535// Mock implementation - no cross-tab sync in tests
3636}
37-
37+
3838addEventListener ( _type :string , _listener :EventListener ) {
3939// Mock implementation
4040}
41-
41+
4242removeEventListener ( _type :string , _listener :EventListener ) {
4343// Mock implementation
4444}
45-
45+
4646dispatchEvent ( _event :Event ) :boolean {
4747// Mock implementation
4848return true ;
4949}
50-
50+
5151close ( ) {
5252// Mock implementation
5353}
@@ -56,8 +56,8 @@ if (typeof global.BroadcastChannel === "undefined") {
5656
5757// Mock console methods to reduce test noise
5858beforeAll ( ( ) => {
59- jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
60- jest . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
59+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
60+ jest . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
6161} ) ;
6262
6363afterAll ( ( ) => {
@@ -181,10 +181,10 @@ function ObjectTestComponent({ storageKey }: { storageKey: string }) {
181181}
182182
183183// Wrapper with Suspense and Error Boundary
184- function SuspenseWrapper ( {
185- children,
186- fallback= "Loading..."
187- } :{
184+ function SuspenseWrapper ( {
185+ children,
186+ fallback= "Loading..."
187+ } :{
188188children :React . ReactNode ;
189189fallback ?:string ;
190190} ) {
@@ -677,4 +677,281 @@ describe("useSuspenseIndexedDBState", () => {
677677expect ( screen . getByTestId ( "user-name" ) . textContent ) . toBe ( "Updated User" ) ;
678678expect ( screen . getByTestId ( "items-count" ) . textContent ) . toBe ( "3" ) ;
679679} ) ;
680+
681+ it ( "should handle broadcast channel messages correctly" , async ( ) => {
682+ expect . hasAssertions ( ) ;
683+
684+ // Create a working BroadcastChannel implementation for testing
685+ const channels :Map < string , MockBroadcastChannel [ ] > = new Map ( ) ;
686+
687+ class MockBroadcastChannel implements BroadcastChannel {
688+ name :string ;
689+ onmessage :( ( event :MessageEvent ) => void ) | null = null ;
690+ onmessageerror :( ( event :MessageEvent ) => void ) | null = null ;
691+ private listeners :EventListener [ ] = [ ] ;
692+
693+ constructor ( name :string ) {
694+ this . name = name ;
695+ if ( ! channels . has ( name ) ) {
696+ channels . set ( name , [ ] ) ;
697+ }
698+ channels . get ( name ) ! . push ( this ) ;
699+ }
700+
701+ postMessage ( message :unknown ) {
702+ const channelInstances = channels . get ( this . name ) || [ ] ;
703+ // Simulate async message delivery
704+ setTimeout ( ( ) => {
705+ channelInstances . forEach ( instance => {
706+ if ( instance !== this ) { // Don't send to self
707+ const event = new MessageEvent ( 'message' , { data :message } ) ;
708+ instance . listeners . forEach ( listener => {
709+ listener ( event ) ;
710+ } ) ;
711+ if ( instance . onmessage ) {
712+ instance . onmessage ( event ) ;
713+ }
714+ }
715+ } ) ;
716+ } , 0 ) ;
717+ }
718+
719+ addEventListener ( type :string , listener :EventListener ) {
720+ if ( type === 'message' ) {
721+ this . listeners . push ( listener ) ;
722+ }
723+ }
724+
725+ removeEventListener ( type :string , listener :EventListener ) {
726+ if ( type === 'message' ) {
727+ const index = this . listeners . indexOf ( listener ) ;
728+ if ( index > - 1 ) {
729+ this . listeners . splice ( index , 1 ) ;
730+ }
731+ }
732+ }
733+
734+ dispatchEvent ( _event :Event ) :boolean {
735+ return true ;
736+ }
737+
738+ close ( ) {
739+ const channelInstances = channels . get ( this . name ) ;
740+ if ( channelInstances ) {
741+ const index = channelInstances . indexOf ( this ) ;
742+ if ( index > - 1 ) {
743+ channelInstances . splice ( index , 1 ) ;
744+ }
745+ }
746+ }
747+ }
748+
749+ // Temporarily replace the global BroadcastChannel
750+ const originalBroadcastChannel = global . BroadcastChannel ;
751+ global . BroadcastChannel = MockBroadcastChannel as any ;
752+
753+ let testChannel :MockBroadcastChannel | null = null ;
754+ testChannel = new MockBroadcastChannel ( "rooks-indexeddb-rooks-db-state" ) ;
755+
756+ const TestBroadcastComponent = ( ) => {
757+ const [ value , { setItem} ] = useSuspenseIndexedDBState (
758+ "broadcast-test" ,
759+ ( current ) => current || "initial"
760+ ) ;
761+
762+ return (
763+ < div >
764+ < div data-testid = "broadcast-value" > { JSON . stringify ( value ) } </ div >
765+ < button
766+ data-testid = "set-broadcast-value"
767+ onClick = { ( ) => setItem ( "local-update" ) }
768+ >
769+ Set Value
770+ </ button >
771+ </ div >
772+ ) ;
773+ } ;
774+
775+ render (
776+ < SuspenseWrapper >
777+ < TestBroadcastComponent />
778+ </ SuspenseWrapper >
779+ ) ;
780+
781+ // Wait for component to load
782+ await waitFor ( ( ) => {
783+ expect ( screen . getByTestId ( "broadcast-value" ) ) . toBeInTheDocument ( ) ;
784+ } ) ;
785+
786+ expect ( screen . getByTestId ( "broadcast-value" ) . textContent ) . toBe ( '"initial"' ) ;
787+
788+ // Simulate broadcast message from another tab
789+ if ( testChannel ) {
790+ await act ( async ( ) => {
791+ testChannel . postMessage ( {
792+ type :'SET' ,
793+ key :'broadcast-test' ,
794+ value :'broadcast-update' ,
795+ dbName :'rooks-db' ,
796+ storeName :'state'
797+ } ) ;
798+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
799+ } ) ;
800+
801+ // Value should be updated from broadcast message
802+ expect ( screen . getByTestId ( "broadcast-value" ) . textContent ) . toBe ( '"broadcast-update"' ) ;
803+
804+ // Test DELETE broadcast message
805+ await act ( async ( ) => {
806+ testChannel . postMessage ( {
807+ type :'DELETE' ,
808+ key :'broadcast-test' ,
809+ dbName :'rooks-db' ,
810+ storeName :'state'
811+ } ) ;
812+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
813+ } ) ;
814+
815+ // Value should be reset to initial value
816+ expect ( screen . getByTestId ( "broadcast-value" ) . textContent ) . toBe ( '"initial"' ) ;
817+
818+ testChannel . close ( ) ;
819+ }
820+
821+ // Restore original BroadcastChannel
822+ global . BroadcastChannel = originalBroadcastChannel ;
823+ } ) ;
824+
825+ it ( "should not respond to broadcast messages for different keys/databases" , async ( ) => {
826+ expect . hasAssertions ( ) ;
827+
828+ // Create a working BroadcastChannel implementation for testing
829+ const channels :Map < string , MockBroadcastChannel [ ] > = new Map ( ) ;
830+
831+ class MockBroadcastChannel implements BroadcastChannel {
832+ name :string ;
833+ onmessage :( ( event :MessageEvent ) => void ) | null = null ;
834+ onmessageerror :( ( event :MessageEvent ) => void ) | null = null ;
835+ private listeners :EventListener [ ] = [ ] ;
836+
837+ constructor ( name :string ) {
838+ this . name = name ;
839+ if ( ! channels . has ( name ) ) {
840+ channels . set ( name , [ ] ) ;
841+ }
842+ channels . get ( name ) ! . push ( this ) ;
843+ }
844+
845+ postMessage ( message :unknown ) {
846+ const channelInstances = channels . get ( this . name ) || [ ] ;
847+ setTimeout ( ( ) => {
848+ channelInstances . forEach ( instance => {
849+ if ( instance !== this ) {
850+ const event = new MessageEvent ( 'message' , { data :message } ) ;
851+ instance . listeners . forEach ( listener => {
852+ listener ( event ) ;
853+ } ) ;
854+ if ( instance . onmessage ) {
855+ instance . onmessage ( event ) ;
856+ }
857+ }
858+ } ) ;
859+ } , 0 ) ;
860+ }
861+
862+ addEventListener ( type :string , listener :EventListener ) {
863+ if ( type === 'message' ) {
864+ this . listeners . push ( listener ) ;
865+ }
866+ }
867+
868+ removeEventListener ( type :string , listener :EventListener ) {
869+ if ( type === 'message' ) {
870+ const index = this . listeners . indexOf ( listener ) ;
871+ if ( index > - 1 ) {
872+ this . listeners . splice ( index , 1 ) ;
873+ }
874+ }
875+ }
876+
877+ dispatchEvent ( _event :Event ) :boolean {
878+ return true ;
879+ }
880+
881+ close ( ) {
882+ const channelInstances = channels . get ( this . name ) ;
883+ if ( channelInstances ) {
884+ const index = channelInstances . indexOf ( this ) ;
885+ if ( index > - 1 ) {
886+ channelInstances . splice ( index , 1 ) ;
887+ }
888+ }
889+ }
890+ }
891+
892+ // Temporarily replace the global BroadcastChannel
893+ const originalBroadcastChannel = global . BroadcastChannel ;
894+ global . BroadcastChannel = MockBroadcastChannel as any ;
895+
896+ let testChannel :MockBroadcastChannel | null = null ;
897+ testChannel = new MockBroadcastChannel ( "rooks-indexeddb-rooks-db-state" ) ;
898+
899+ const TestBroadcastComponent = ( ) => {
900+ const [ value ] = useSuspenseIndexedDBState (
901+ "specific-key" ,
902+ ( current ) => current || "original"
903+ ) ;
904+
905+ return < div data-testid = "specific-value" > { JSON . stringify ( value ) } </ div > ;
906+ } ;
907+
908+ render (
909+ < SuspenseWrapper >
910+ < TestBroadcastComponent />
911+ </ SuspenseWrapper >
912+ ) ;
913+
914+ await waitFor ( ( ) => {
915+ expect ( screen . getByTestId ( "specific-value" ) ) . toBeInTheDocument ( ) ;
916+ } ) ;
917+
918+ expect ( screen . getByTestId ( "specific-value" ) . textContent ) . toBe ( '"original"' ) ;
919+
920+ if ( testChannel ) {
921+ // Send message for different key - should not affect our component
922+ await act ( async ( ) => {
923+ testChannel . postMessage ( {
924+ type :'SET' ,
925+ key :'different-key' ,
926+ value :'should-not-update' ,
927+ dbName :'rooks-db' ,
928+ storeName :'state'
929+ } ) ;
930+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
931+ } ) ;
932+
933+ // Value should remain unchanged
934+ expect ( screen . getByTestId ( "specific-value" ) . textContent ) . toBe ( '"original"' ) ;
935+
936+ // Send message for different database - should not affect our component
937+ await act ( async ( ) => {
938+ testChannel . postMessage ( {
939+ type :'SET' ,
940+ key :'specific-key' ,
941+ value :'should-not-update' ,
942+ dbName :'different-db' ,
943+ storeName :'state'
944+ } ) ;
945+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
946+ } ) ;
947+
948+ // Value should remain unchanged
949+ expect ( screen . getByTestId ( "specific-value" ) . textContent ) . toBe ( '"original"' ) ;
950+
951+ testChannel . close ( ) ;
952+ }
953+
954+ // Restore original BroadcastChannel
955+ global . BroadcastChannel = originalBroadcastChannel ;
956+ } ) ;
680957} ) ;