@@ -297,6 +297,188 @@ describe('SessionManager', () => {
297297 } ) ;
298298 } ) ;
299299
300+ describe ( '#hasSessionTimedOut' , ( ) => {
301+ it ( 'should return true when elapsed time exceeds session timeout' , ( ) => {
302+ const timePassed = 35 * ( MILLIS_IN_ONE_SEC * 60 ) ; // 35 minutes
303+
304+ mParticle . init ( apiKey , window . mParticle . config ) ;
305+ const mpInstance = mParticle . getInstance ( ) ;
306+
307+ mpInstance . _Store . sessionId = 'OLD-ID' ;
308+ const timeLastEventSent = mpInstance . _Store . dateLastEventSent . getTime ( ) ;
309+ mpInstance . _Store . dateLastEventSent = new Date ( timeLastEventSent - timePassed ) ;
310+
311+ // initialize() uses hasSessionTimedOut internally
312+ mpInstance . _SessionManager . initialize ( ) ;
313+
314+ // Should have created a new session because timeout was exceeded
315+ expect ( mpInstance . _Store . sessionId ) . to . not . equal ( 'OLD-ID' ) ;
316+ } ) ;
317+
318+ it ( 'should return false when elapsed time is within session timeout' , ( ) => {
319+ const timePassed = 15 * ( MILLIS_IN_ONE_SEC * 60 ) ; // 15 minutes
320+
321+ mParticle . init ( apiKey , window . mParticle . config ) ;
322+ const mpInstance = mParticle . getInstance ( ) ;
323+
324+ mpInstance . _Store . sessionId = 'OLD-ID' ;
325+ const timeLastEventSent = mpInstance . _Store . dateLastEventSent . getTime ( ) ;
326+ mpInstance . _Store . dateLastEventSent = new Date ( timeLastEventSent - timePassed ) ;
327+
328+ // initialize() uses hasSessionTimedOut internally
329+ mpInstance . _SessionManager . initialize ( ) ;
330+
331+ // Should have kept the old session because timeout was not exceeded
332+ expect ( mpInstance . _Store . sessionId ) . to . equal ( 'OLD-ID' ) ;
333+ } ) ;
334+
335+ it ( 'should work consistently with both in-memory and persisted timestamps' , ( ) => {
336+ const now = new Date ( ) ;
337+ const thirtyOneMinutesAgo = new Date ( ) ;
338+ thirtyOneMinutesAgo . setMinutes ( now . getMinutes ( ) - 31 ) ;
339+
340+ mParticle . init ( apiKey , window . mParticle . config ) ;
341+ const mpInstance = mParticle . getInstance ( ) ;
342+
343+ // Test with in-memory store (via initialize)
344+ mpInstance . _Store . sessionId = 'TEST-ID' ;
345+ mpInstance . _Store . dateLastEventSent = thirtyOneMinutesAgo ;
346+ mpInstance . _SessionManager . initialize ( ) ;
347+
348+ // Session should have expired (default timeout is 30 minutes)
349+ expect ( mpInstance . _Store . sessionId ) . to . not . equal ( 'TEST-ID' ) ;
350+
351+ // Test with persistence (via endSession)
352+ const newSessionId = mpInstance . _Store . sessionId ;
353+ sinon . stub ( mpInstance . _Persistence , 'getPersistence' ) . returns ( {
354+ gs : {
355+ les : thirtyOneMinutesAgo . getTime ( ) ,
356+ sid : newSessionId ,
357+ } ,
358+ } ) ;
359+
360+ mpInstance . _SessionManager . endSession ( ) ;
361+
362+ // Session should have ended (same timeout logic)
363+ expect ( mpInstance . _Store . sessionId ) . to . equal ( null ) ;
364+ } ) ;
365+
366+ it ( 'should return true when elapsed time equals session timeout exactly' , ( ) => {
367+ const now = new Date ( ) ;
368+ const exactlyThirtyMinutesAgo = new Date ( ) ;
369+ exactlyThirtyMinutesAgo . setMinutes ( now . getMinutes ( ) - 30 ) ;
370+
371+ mParticle . init ( apiKey , window . mParticle . config ) ;
372+ const mpInstance = mParticle . getInstance ( ) ;
373+
374+ sinon . stub ( mpInstance . _Persistence , 'getPersistence' ) . returns ( {
375+ gs : {
376+ les : exactlyThirtyMinutesAgo . getTime ( ) ,
377+ sid : 'TEST-ID' ,
378+ } ,
379+ } ) ;
380+
381+ mpInstance . _SessionManager . endSession ( ) ;
382+
383+ // At exactly 30 minutes, session should be expired
384+ expect ( mpInstance . _Store . sessionId ) . to . equal ( null ) ;
385+ } ) ;
386+ } ) ;
387+
388+ describe ( '#performSessionEnd' , ( ) => {
389+ it ( 'should log a SessionEnd event' , ( ) => {
390+ mParticle . init ( apiKey , window . mParticle . config ) ;
391+ const mpInstance = mParticle . getInstance ( ) ;
392+ const eventSpy = sinon . spy ( mpInstance . _Events , 'logEvent' ) ;
393+
394+ mpInstance . _SessionManager . endSession ( true ) ;
395+
396+ // Find the SessionEnd event call
397+ const sessionEndCall = eventSpy . getCalls ( ) . find ( call =>
398+ call . args [ 0 ] ?. messageType === MessageType . SessionEnd
399+ ) ;
400+
401+ expect ( sessionEndCall ) . to . not . be . undefined ;
402+ expect ( sessionEndCall . args [ 0 ] ) . to . eql ( {
403+ messageType : MessageType . SessionEnd ,
404+ } ) ;
405+ } ) ;
406+
407+ it ( 'should clear sessionStartDate' , ( ) => {
408+ mParticle . init ( apiKey , window . mParticle . config ) ;
409+ const mpInstance = mParticle . getInstance ( ) ;
410+
411+ const sessionStartDate = mpInstance . _Store . sessionStartDate ;
412+ expect ( sessionStartDate ) . to . not . be . null ;
413+
414+ mpInstance . _SessionManager . endSession ( true ) ;
415+
416+ expect ( mpInstance . _Store . sessionStartDate ) . to . equal ( null ) ;
417+ } ) ;
418+
419+ it ( 'should nullify session ID and session attributes' , ( ) => {
420+ mParticle . init ( apiKey , window . mParticle . config ) ;
421+ const mpInstance = mParticle . getInstance ( ) ;
422+
423+ // Set up session data
424+ mpInstance . _Store . sessionAttributes = { testAttr : 'value' } ;
425+ mpInstance . _Store . localSessionAttributes = { localAttr : 'value' } ;
426+
427+ expect ( mpInstance . _Store . sessionId ) . to . not . be . null ;
428+
429+ mpInstance . _SessionManager . endSession ( true ) ;
430+
431+ expect ( mpInstance . _Store . sessionId ) . to . equal ( null ) ;
432+ expect ( mpInstance . _Store . sessionAttributes ) . to . eql ( { } ) ;
433+ expect ( mpInstance . _Store . localSessionAttributes ) . to . eql ( { } ) ;
434+ } ) ;
435+
436+ it ( 'should reset timeOnSiteTimer if it exists' , ( ) => {
437+ mParticle . init ( apiKey , window . mParticle . config ) ;
438+ const mpInstance = mParticle . getInstance ( ) ;
439+
440+ // Timer should exist since workspaceToken is present in config
441+ expect ( mpInstance . _timeOnSiteTimer ) . to . exist ;
442+
443+ const resetTimerSpy = sinon . spy ( mpInstance . _timeOnSiteTimer , 'resetTimer' ) ;
444+
445+ mpInstance . _SessionManager . endSession ( true ) ;
446+
447+ expect ( resetTimerSpy . called ) . to . equal ( true ) ;
448+ } ) ;
449+
450+ it ( 'should handle missing timeOnSiteTimer gracefully' , ( ) => {
451+ mParticle . init ( apiKey , window . mParticle . config ) ;
452+ const mpInstance = mParticle . getInstance ( ) ;
453+
454+ // Explicitly remove the timer to test the optional chaining behavior
455+ mpInstance . _timeOnSiteTimer = undefined ;
456+
457+ expect ( ( ) => {
458+ mpInstance . _SessionManager . endSession ( true ) ;
459+ } ) . to . not . throw ( ) ;
460+
461+ // Session should still end properly
462+ expect ( mpInstance . _Store . sessionId ) . to . equal ( null ) ;
463+ } ) ;
464+
465+ it ( 'should perform all session end operations' , ( ) => {
466+ mParticle . init ( apiKey , window . mParticle . config ) ;
467+ const mpInstance = mParticle . getInstance ( ) ;
468+
469+ const eventSpy = sinon . spy ( mpInstance . _Events , 'logEvent' ) ;
470+ const persistenceSpy = sinon . spy ( mpInstance . _Persistence , 'update' ) ;
471+
472+ mpInstance . _SessionManager . endSession ( true ) ;
473+
474+ // Verify all operations happened
475+ expect ( eventSpy . called ) . to . equal ( true ) ;
476+ expect ( mpInstance . _Store . sessionStartDate ) . to . equal ( null ) ;
477+ expect ( mpInstance . _Store . sessionId ) . to . equal ( null ) ;
478+ expect ( persistenceSpy . called ) . to . equal ( true ) ;
479+ } ) ;
480+ } ) ;
481+
300482 describe ( '#endSession' , ( ) => {
301483 it ( 'should end a session' , ( ) => {
302484 mParticle . init ( apiKey , window . mParticle . config ) ;
0 commit comments