@@ -659,12 +659,221 @@ describe('actor restart — StrictMode start/stop/start cycle', () => {
659659 // Stopped mid-invoke (e.g. Suspense remount) then restarted.
660660 actor . stop ( ) ;
661661 actor . start ( ) ;
662- expect ( actor . getSnapshot ( ) . status ) . toBe ( ' active') ;
662+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'deleting' , status : ' active' } ) ;
663663 // The invoke must not fire a second time.
664664 expect ( invokeCount ) . toBe ( 1 ) ;
665665 } ) ;
666666} ) ;
667667
668+ describe ( 'actor restart — stop/start resumes the current state' , ( ) => {
669+ it ( 'stays in a state without an invoke' , ( ) => {
670+ const actor = createActor ( createDeleteOrgMachine ( ( ) => Promise . resolve ( ) ) ) ;
671+ actor . start ( ) ;
672+ actor . send ( { type : 'OPEN' } ) ;
673+ actor . send ( { type : 'TYPE' , value : 'Acme' } ) ;
674+
675+ actor . stop ( ) ;
676+ actor . start ( ) ;
677+
678+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'confirming' , status : 'active' } ) ;
679+ expect ( actor . getSnapshot ( ) . context . confirmValue ) . toBe ( 'Acme' ) ;
680+ actor . send ( { type : 'CANCEL' } ) ;
681+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'idle' ) ;
682+ } ) ;
683+
684+ it ( 'applies an invoke that resolved while stopped once started again' , async ( ) => {
685+ const gate = deferred < string > ( ) ;
686+ const actor = createActor ( createLoaderMachine ( ( ) => gate . promise ) ) ;
687+ actor . start ( ) ;
688+ actor . send ( { type : 'FETCH' } ) ;
689+
690+ actor . stop ( ) ;
691+ gate . resolve ( 'loaded' ) ;
692+ await tick ( ) ;
693+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'loading' , status : 'stopped' } ) ;
694+
695+ actor . start ( ) ;
696+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'success' , status : 'done' } ) ;
697+ expect ( actor . getSnapshot ( ) . context . data ) . toBe ( 'loaded' ) ;
698+ } ) ;
699+
700+ it ( 'applies an invoke that rejected while stopped once started again' , async ( ) => {
701+ const gate = deferred < string > ( ) ;
702+ const actor = createActor ( createLoaderMachine ( ( ) => gate . promise ) ) ;
703+ actor . start ( ) ;
704+ actor . send ( { type : 'FETCH' } ) ;
705+
706+ actor . stop ( ) ;
707+ gate . reject ( 'boom' ) ;
708+ await tick ( ) ;
709+ actor . start ( ) ;
710+
711+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'failure' , status : 'active' } ) ;
712+ expect ( actor . getSnapshot ( ) . context . error ) . toBe ( 'boom' ) ;
713+ } ) ;
714+
715+ it ( 'handles an invoke that settles after the restart exactly once' , async ( ) => {
716+ const gate = deferred < string > ( ) ;
717+ const seen : string [ ] = [ ] ;
718+ const actor = createActor ( createLoaderMachine ( ( ) => gate . promise ) ) ;
719+ actor . start ( ) ;
720+ actor . send ( { type : 'FETCH' } ) ;
721+
722+ actor . stop ( ) ;
723+ actor . start ( ) ;
724+ actor . subscribe ( snapshot => seen . push ( snapshot . value ) ) ;
725+ gate . resolve ( 'late' ) ;
726+ await tick ( ) ;
727+
728+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'success' ) ;
729+ expect ( actor . getSnapshot ( ) . context . data ) . toBe ( 'late' ) ;
730+ expect ( seen ) . toEqual ( [ 'success' ] ) ;
731+ } ) ;
732+
733+ it ( 'runs entry actions and the invoke once across a StrictMode start/stop/start' , async ( ) => {
734+ const entry = vi . fn ( ) ;
735+ const src = vi . fn ( ( ) => Promise . resolve ( 'ok' ) ) ;
736+ const machine = createMachine < { out : string } , { type : 'never' } > ( {
737+ initial : 'working' ,
738+ context : { out : '' } ,
739+ states : {
740+ working : {
741+ entry,
742+ invoke : {
743+ src,
744+ onDone : {
745+ target : 'done' ,
746+ actions : assign < { out : string } , DoneInvokeEvent < string > > ( ( _ , event ) => ( { out : event . output } ) ) ,
747+ } ,
748+ } ,
749+ } ,
750+ done : { type : 'final' } ,
751+ } ,
752+ } ) ;
753+ const actor = createActor ( machine ) ;
754+
755+ actor . start ( ) ;
756+ actor . stop ( ) ;
757+ actor . start ( ) ;
758+ await tick ( ) ;
759+
760+ expect ( entry ) . toHaveBeenCalledTimes ( 1 ) ;
761+ expect ( src ) . toHaveBeenCalledTimes ( 1 ) ;
762+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'done' , status : 'done' , context : { out : 'ok' } } ) ;
763+ } ) ;
764+
765+ it ( 'never notifies or schedules work once stopped for good' , async ( ) => {
766+ vi . useFakeTimers ( ) ;
767+ try {
768+ const gate = deferred < string > ( ) ;
769+ const machine = createMachine < Record < string , never > , { type : 'never' } > ( {
770+ initial : 'working' ,
771+ states : {
772+ working : { invoke : { src : ( ) => gate . promise , onDone : 'done' } , after : { 1000 : 'done' } } ,
773+ done : { } ,
774+ } ,
775+ } ) ;
776+ const actor = createActor ( machine ) ;
777+ actor . start ( ) ;
778+ const listener = vi . fn ( ) ;
779+ actor . subscribe ( listener ) ;
780+
781+ actor . stop ( ) ;
782+ listener . mockClear ( ) ;
783+ expect ( vi . getTimerCount ( ) ) . toBe ( 0 ) ;
784+
785+ gate . resolve ( 'ignored' ) ;
786+ await vi . runAllTimersAsync ( ) ;
787+
788+ expect ( listener ) . not . toHaveBeenCalled ( ) ;
789+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'working' , status : 'stopped' } ) ;
790+ } finally {
791+ vi . useRealTimers ( ) ;
792+ }
793+ } ) ;
794+
795+ it ( 'queues events sent while stopped and applies them in order on start' , ( ) => {
796+ const actor = createActor ( createDeleteOrgMachine ( ( ) => Promise . resolve ( ) ) ) ;
797+ actor . start ( ) ;
798+ actor . stop ( ) ;
799+ const listener = vi . fn ( ) ;
800+ actor . subscribe ( listener ) ;
801+
802+ actor . send ( { type : 'OPEN' } ) ;
803+ actor . send ( { type : 'TYPE' , value : 'Acme' } ) ;
804+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'idle' , status : 'stopped' } ) ;
805+ expect ( listener ) . not . toHaveBeenCalled ( ) ;
806+
807+ actor . start ( ) ;
808+
809+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'confirming' , status : 'active' } ) ;
810+ expect ( actor . getSnapshot ( ) . context . confirmValue ) . toBe ( 'Acme' ) ;
811+ expect ( listener ) . toHaveBeenCalledTimes ( 1 ) ;
812+ } ) ;
813+
814+ it ( 'applies an invoke result and events in the order they arrived while stopped' , async ( ) => {
815+ const gate = deferred < void > ( ) ;
816+ const actor = createActor ( createDeleteOrgMachine ( ( ) => gate . promise ) ) ;
817+ actor . start ( ) ;
818+ actor . send ( { type : 'OPEN' } ) ;
819+ actor . send ( { type : 'TYPE' , value : 'Acme Inc' } ) ;
820+ actor . send ( { type : 'CONFIRM' } ) ;
821+
822+ actor . stop ( ) ;
823+ gate . reject ( 'boom' ) ;
824+ await tick ( ) ;
825+ actor . send ( { type : 'TYPE' , value : 'retry' } ) ;
826+ actor . start ( ) ;
827+
828+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( {
829+ value : 'confirming' ,
830+ context : { error : 'boom' , confirmValue : 'retry' } ,
831+ } ) ;
832+ } ) ;
833+
834+ it ( 'drops an invoke result when an event queued before it leaves the invoking state' , async ( ) => {
835+ const gate = deferred < string > ( ) ;
836+ const machine = createMachine < Record < string , never > , { type : 'GO' } | { type : 'CANCEL' } > ( {
837+ initial : 'idle' ,
838+ states : {
839+ idle : { on : { GO : 'working' } } ,
840+ working : { invoke : { src : ( ) => gate . promise , onDone : 'done' } , on : { CANCEL : 'idle' } } ,
841+ done : { } ,
842+ } ,
843+ } ) ;
844+ const actor = createActor ( machine ) ;
845+ actor . start ( ) ;
846+ actor . send ( { type : 'GO' } ) ;
847+
848+ actor . stop ( ) ;
849+ actor . send ( { type : 'CANCEL' } ) ;
850+ gate . resolve ( 'late' ) ;
851+ await tick ( ) ;
852+ actor . start ( ) ;
853+
854+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'idle' ) ;
855+ } ) ;
856+
857+ it ( 'ignores send before the first start' , ( ) => {
858+ const actor = createActor ( createDeleteOrgMachine ( ( ) => Promise . resolve ( ) ) ) ;
859+ actor . send ( { type : 'OPEN' } ) ;
860+ actor . start ( ) ;
861+
862+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'idle' ) ;
863+ } ) ;
864+
865+ it ( 'keeps a teleported actor inert across a restart' , ( ) => {
866+ const src = vi . fn ( ( ) => Promise . resolve ( ) ) ;
867+ const actor = mockActor ( createDeleteOrgMachine ( src ) , { value : 'deleting' , context : { error : 'shown' } } ) ;
868+
869+ actor . stop ( ) ;
870+ actor . start ( ) ;
871+
872+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'deleting' , status : 'active' , context : { error : 'shown' } } ) ;
873+ expect ( src ) . not . toHaveBeenCalled ( ) ;
874+ } ) ;
875+ } ) ;
876+
668877describe ( 'createActor — after (delayed transitions)' , ( ) => {
669878 beforeEach ( ( ) => {
670879 vi . useFakeTimers ( ) ;
@@ -847,6 +1056,30 @@ describe('createActor — after (delayed transitions)', () => {
8471056 vi . advanceTimersByTime ( 60_000 ) ;
8481057 expect ( actor . getSnapshot ( ) . value ) . toBe ( 'expired' ) ;
8491058 } ) ;
1059+
1060+ it ( 'does not fire while stopped and re-arms the timer of the current state on start' , ( ) => {
1061+ const machine = createMachine < Record < string , never > , { type : 'SEND' } > ( {
1062+ initial : 'idle' ,
1063+ states : {
1064+ idle : { on : { SEND : 'codeSent' } } ,
1065+ codeSent : { after : { 60_000 : 'expired' } } ,
1066+ expired : { } ,
1067+ } ,
1068+ } ) ;
1069+ const actor = createActor ( machine ) ;
1070+ actor . start ( ) ;
1071+ actor . send ( { type : 'SEND' } ) ;
1072+
1073+ actor . stop ( ) ;
1074+ vi . advanceTimersByTime ( 120_000 ) ;
1075+ expect ( actor . getSnapshot ( ) ) . toMatchObject ( { value : 'codeSent' , status : 'stopped' } ) ;
1076+
1077+ actor . start ( ) ;
1078+ vi . advanceTimersByTime ( 59_999 ) ;
1079+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'codeSent' ) ;
1080+ vi . advanceTimersByTime ( 1 ) ;
1081+ expect ( actor . getSnapshot ( ) . value ) . toBe ( 'expired' ) ;
1082+ } ) ;
8501083} ) ;
8511084
8521085describe ( 'createActor — inline transition functions (TransitionFn)' , ( ) => {
0 commit comments