@@ -189,6 +189,234 @@ describe('devtools', () => {
189189 expect ( store . doubled ) . toBe ( 6 ) ;
190190 } ) ;
191191
192+ it ( 'handles JUMP_TO_ACTION time-travel' , async ( ) => {
193+ const conn = createMockConnection ( ) ;
194+ const ext = createMockExtension ( conn ) ;
195+ setExtension ( ext ) ;
196+
197+ class Store {
198+ count = 0 ;
199+ }
200+
201+ const store = createClassyStore ( new Store ( ) ) ;
202+ devtools ( store ) ;
203+
204+ store . count = 10 ;
205+ await tick ( ) ;
206+
207+ conn . _listener ?.( {
208+ type : 'DISPATCH' ,
209+ payload : { type : 'JUMP_TO_ACTION' } ,
210+ state : JSON . stringify ( { count : 3 } ) ,
211+ } ) ;
212+ await tick ( ) ;
213+
214+ expect ( store . count ) . toBe ( 3 ) ;
215+ } ) ;
216+
217+ it ( 'skips methods during time-travel restore' , async ( ) => {
218+ const conn = createMockConnection ( ) ;
219+ const ext = createMockExtension ( conn ) ;
220+ setExtension ( ext ) ;
221+
222+ class Store {
223+ count = 0 ;
224+ increment ( ) {
225+ this . count ++ ;
226+ }
227+ }
228+
229+ const store = createClassyStore ( new Store ( ) ) ;
230+ devtools ( store ) ;
231+
232+ conn . _listener ?.( {
233+ type : 'DISPATCH' ,
234+ payload : { type : 'JUMP_TO_STATE' } ,
235+ state : JSON . stringify ( { count : 5 , increment : 'should be skipped' } ) ,
236+ } ) ;
237+ await tick ( ) ;
238+
239+ expect ( store . count ) . toBe ( 5 ) ;
240+ expect ( typeof store . increment ) . toBe ( 'function' ) ;
241+ } ) ;
242+
243+ it ( 'does not send state updates during time-travel' , async ( ) => {
244+ const conn = createMockConnection ( ) ;
245+ const ext = createMockExtension ( conn ) ;
246+ setExtension ( ext ) ;
247+
248+ class Store {
249+ count = 0 ;
250+ }
251+
252+ const store = createClassyStore ( new Store ( ) ) ;
253+ devtools ( store ) ;
254+
255+ store . count = 5 ;
256+ await tick ( ) ;
257+ const sendCountBefore = conn . send . mock . calls . length ;
258+
259+ // Simulate time-travel — should NOT trigger a send
260+ conn . _listener ?.( {
261+ type : 'DISPATCH' ,
262+ payload : { type : 'JUMP_TO_STATE' } ,
263+ state : JSON . stringify ( { count : 0 } ) ,
264+ } ) ;
265+ await tick ( ) ;
266+
267+ expect ( conn . send . mock . calls . length ) . toBe ( sendCountBefore ) ;
268+ } ) ;
269+
270+ it ( 'ignores non-DISPATCH messages' , async ( ) => {
271+ const conn = createMockConnection ( ) ;
272+ const ext = createMockExtension ( conn ) ;
273+ setExtension ( ext ) ;
274+
275+ class Store {
276+ count = 0 ;
277+ }
278+
279+ const store = createClassyStore ( new Store ( ) ) ;
280+ devtools ( store ) ;
281+
282+ conn . _listener ?.( {
283+ type : 'ACTION' ,
284+ state : JSON . stringify ( { count : 999 } ) ,
285+ } ) ;
286+ await tick ( ) ;
287+
288+ expect ( store . count ) . toBe ( 0 ) ;
289+ } ) ;
290+
291+ it ( 'ignores DISPATCH messages without state' , async ( ) => {
292+ const conn = createMockConnection ( ) ;
293+ const ext = createMockExtension ( conn ) ;
294+ setExtension ( ext ) ;
295+
296+ class Store {
297+ count = 0 ;
298+ }
299+
300+ const store = createClassyStore ( new Store ( ) ) ;
301+ devtools ( store ) ;
302+
303+ conn . _listener ?.( {
304+ type : 'DISPATCH' ,
305+ payload : { type : 'JUMP_TO_STATE' } ,
306+ // no state field
307+ } ) ;
308+ await tick ( ) ;
309+
310+ expect ( store . count ) . toBe ( 0 ) ;
311+ } ) ;
312+
313+ it ( 'ignores DISPATCH messages with non-jump payload types' , async ( ) => {
314+ const conn = createMockConnection ( ) ;
315+ const ext = createMockExtension ( conn ) ;
316+ setExtension ( ext ) ;
317+
318+ class Store {
319+ count = 0 ;
320+ }
321+
322+ const store = createClassyStore ( new Store ( ) ) ;
323+ devtools ( store ) ;
324+
325+ conn . _listener ?.( {
326+ type : 'DISPATCH' ,
327+ payload : { type : 'COMMIT' } ,
328+ state : JSON . stringify ( { count : 999 } ) ,
329+ } ) ;
330+ await tick ( ) ;
331+
332+ expect ( store . count ) . toBe ( 0 ) ;
333+ } ) ;
334+
335+ it ( 'handles corrupted JSON in time-travel state gracefully' , async ( ) => {
336+ const conn = createMockConnection ( ) ;
337+ const ext = createMockExtension ( conn ) ;
338+ setExtension ( ext ) ;
339+
340+ class Store {
341+ count = 5 ;
342+ }
343+
344+ const store = createClassyStore ( new Store ( ) ) ;
345+ devtools ( store ) ;
346+
347+ // Should not throw
348+ conn . _listener ?.( {
349+ type : 'DISPATCH' ,
350+ payload : { type : 'JUMP_TO_STATE' } ,
351+ state : 'not-valid-json!!!' ,
352+ } ) ;
353+ await tick ( ) ;
354+
355+ expect ( store . count ) . toBe ( 5 ) ; // unchanged
356+ } ) ;
357+
358+ it ( 'uses default name ClassyStore when no name provided' , ( ) => {
359+ const conn = createMockConnection ( ) ;
360+ const ext = createMockExtension ( conn ) ;
361+ setExtension ( ext ) ;
362+
363+ class Store {
364+ count = 0 ;
365+ }
366+
367+ const store = createClassyStore ( new Store ( ) ) ;
368+ devtools ( store ) ;
369+
370+ expect ( ext . connect ) . toHaveBeenCalledWith ( { name : 'ClassyStore' } ) ;
371+ } ) ;
372+
373+ it ( 'handles connection.subscribe returning {unsubscribe} object' , async ( ) => {
374+ const unsubMock = mock ( ( ) => { } ) ;
375+ const conn = createMockConnection ( ) ;
376+ // Override subscribe to return an object with unsubscribe method
377+ conn . subscribe = mock ( ( listener : ( message : unknown ) => void ) => {
378+ conn . _listener = listener ;
379+ return { unsubscribe : unsubMock } ;
380+ } ) ;
381+ const ext = createMockExtension ( conn ) ;
382+ setExtension ( ext ) ;
383+
384+ class Store {
385+ count = 0 ;
386+ }
387+
388+ const store = createClassyStore ( new Store ( ) ) ;
389+ const dispose = devtools ( store ) ;
390+
391+ dispose ( ) ;
392+
393+ expect ( unsubMock ) . toHaveBeenCalledTimes ( 1 ) ;
394+ } ) ;
395+
396+ it ( 'sends multiple state updates for sequential mutations' , async ( ) => {
397+ const conn = createMockConnection ( ) ;
398+ const ext = createMockExtension ( conn ) ;
399+ setExtension ( ext ) ;
400+
401+ class Store {
402+ count = 0 ;
403+ }
404+
405+ const store = createClassyStore ( new Store ( ) ) ;
406+ devtools ( store ) ;
407+
408+ store . count = 1 ;
409+ await tick ( ) ;
410+ store . count = 2 ;
411+ await tick ( ) ;
412+ store . count = 3 ;
413+ await tick ( ) ;
414+
415+ expect ( conn . send ) . toHaveBeenCalledTimes ( 3 ) ;
416+ const lastState = conn . send . mock . calls [ 2 ] [ 1 ] as Record < string , unknown > ;
417+ expect ( lastState . count ) . toBe ( 3 ) ;
418+ } ) ;
419+
192420 it ( 'disposes correctly (unsubscribes from store and devtools)' , async ( ) => {
193421 const conn = createMockConnection ( ) ;
194422 const ext = createMockExtension ( conn ) ;
0 commit comments