@@ -473,6 +473,138 @@ describe('Framecast', () => {
473473 } ) ;
474474 } ) ;
475475
476+ describe ( 'ready handshake' , ( ) => {
477+ it ( 'signalReady registers function handler and broadcasts' , ( ) => {
478+ framecast . signalReady ( ) ;
479+
480+ // Should broadcast the ready message
481+ const calls = mockTargetWindow . postMessage . mock . calls ;
482+ const readyBroadcast = calls . find ( call => {
483+ const message = superjson . parse ( call [ 0 ] ) as any ;
484+ return message . type === 'broadcast' && message . data ?. type === '__framecast_ready' ;
485+ } ) ;
486+
487+ expect ( readyBroadcast ) . toBeDefined ( ) ;
488+ } ) ;
489+
490+ it ( 'signalReady responds to __framecast_ready function calls' , async ( ) => {
491+ framecast . signalReady ( ) ;
492+
493+ // Simulate a ready check function call
494+ simulateMessage ( 'function:__framecast_ready' , { id : 'ready-check-1' , args : [ ] } ) ;
495+
496+ // Should respond with functionResult
497+ await vi . waitFor ( ( ) => {
498+ const calls = mockTargetWindow . postMessage . mock . calls ;
499+ const resultCall = calls . find ( call => {
500+ const message = superjson . parse ( call [ 0 ] ) as any ;
501+ return message . type === 'functionResult' && message . id === 'ready-check-1' && message . result === true ;
502+ } ) ;
503+ expect ( resultCall ) . toBeDefined ( ) ;
504+ } ) ;
505+ } ) ;
506+
507+ it ( 'waitForReady resolves when it receives a ready broadcast' , async ( ) => {
508+ const readyPromise = framecast . waitForReady ( { interval : 10 , timeout : 1000 } ) ;
509+
510+ // Simulate the ready broadcast from the iframe
511+ simulateMessage ( 'broadcast' , { data : { type : '__framecast_ready' } } ) ;
512+
513+ await expect ( readyPromise ) . resolves . toBeUndefined ( ) ;
514+ } ) ;
515+
516+ it ( 'waitForReady resolves when __framecast_ready function call succeeds' , async ( ) => {
517+ const readyPromise = framecast . waitForReady ( { interval : 10 , timeout : 1000 } ) ;
518+
519+ // Get the function call that was sent (poll)
520+ await vi . waitFor ( ( ) => {
521+ const calls = mockTargetWindow . postMessage . mock . calls ;
522+ const readyCall = calls . find ( call => {
523+ const message = superjson . parse ( call [ 0 ] ) as any ;
524+ return message . type === 'function:__framecast_ready' ;
525+ } ) ;
526+ expect ( readyCall ) . toBeDefined ( ) ;
527+ } ) ;
528+
529+ // Get the call ID and simulate a successful response
530+ const calls = mockTargetWindow . postMessage . mock . calls ;
531+ const readyCall = calls . find ( call => {
532+ const message = superjson . parse ( call [ 0 ] ) as any ;
533+ return message . type === 'function:__framecast_ready' ;
534+ } ) ! ;
535+ const sentMessage = superjson . parse ( readyCall [ 0 ] ) as any ;
536+
537+ simulateMessage ( 'functionResult' , { id : sentMessage . id , result : true } ) ;
538+
539+ await expect ( readyPromise ) . resolves . toBeUndefined ( ) ;
540+ } ) ;
541+
542+ it ( 'waitForReady times out if no ready signal' , async ( ) => {
543+ const readyPromise = framecast . waitForReady ( { interval : 10 , timeout : 100 } ) ;
544+
545+ await expect ( readyPromise ) . rejects . toThrow ( 'waitForReady timed out after 100ms' ) ;
546+ } ) ;
547+
548+ it ( 'waitForReady does not timeout when timeout is 0' , async ( ) => {
549+ const readyPromise = framecast . waitForReady ( { interval : 10 , timeout : 0 } ) ;
550+
551+ // Simulate ready after a short delay
552+ setTimeout ( ( ) => {
553+ simulateMessage ( 'broadcast' , { data : { type : '__framecast_ready' } } ) ;
554+ } , 50 ) ;
555+
556+ await expect ( readyPromise ) . resolves . toBeUndefined ( ) ;
557+ } ) ;
558+
559+ it ( 'full handshake: signalReady + waitForReady' , async ( ) => {
560+ // Create a second framecast pair simulating parent <-> iframe
561+ const iframeMessageHandlers = new Map < string , Function > ( ) ;
562+ const mockIframeWindow : MockWindow = {
563+ postMessage : vi . fn ( ) ,
564+ addEventListener : vi . fn ( ( type : string , handler : Function ) => {
565+ iframeMessageHandlers . set ( type , handler ) ;
566+ } ) ,
567+ removeEventListener : vi . fn ( ) ,
568+ setTimeout : global . setTimeout ,
569+ clearTimeout : global . clearTimeout ,
570+ } ;
571+
572+ // Parent framecast targets iframe window
573+ const parentFramecast = new Framecast ( mockIframeWindow as any , {
574+ self : mockSelfWindow as any ,
575+ functionTimeoutMs : 1000 ,
576+ } ) ;
577+
578+ // Iframe framecast targets parent (mockSelfWindow)
579+ const iframeFramecast = new Framecast ( mockSelfWindow as any , {
580+ self : mockIframeWindow as any ,
581+ functionTimeoutMs : 1000 ,
582+ } ) ;
583+
584+ // Wire up message forwarding: when parent posts to iframe, deliver it
585+ mockIframeWindow . postMessage . mockImplementation ( ( data : string , _origin : string ) => {
586+ const handler = iframeMessageHandlers . get ( 'message' ) ;
587+ handler ?.( { data, origin : '*' } ) ;
588+ } ) ;
589+
590+ // Wire up message forwarding: when iframe posts to parent, deliver it
591+ mockSelfWindow . postMessage . mockImplementation ( ( data : string , _origin : string ) => {
592+ const handler = messageHandlers . get ( 'message' ) ;
593+ handler ?.( { data, origin : '*' } ) ;
594+ } ) ;
595+
596+ // Parent waits for ready
597+ const readyPromise = parentFramecast . waitForReady ( { interval : 10 , timeout : 1000 } ) ;
598+
599+ // Iframe signals ready after a short delay
600+ setTimeout ( ( ) => {
601+ iframeFramecast . signalReady ( ) ;
602+ } , 50 ) ;
603+
604+ await expect ( readyPromise ) . resolves . toBeUndefined ( ) ;
605+ } ) ;
606+ } ) ;
607+
476608 describe ( 'state management' , ( ) => {
477609 it ( 'creates state atoms with initial values' , ( ) => {
478610 const initialValue = { count : 0 } ;
0 commit comments