@@ -54,8 +54,9 @@ vi.mock('../src/services/calendar-service', () => ({
5454 } ,
5555 getStatusText : vi . fn ( ( status : string ) => {
5656 switch ( status ) {
57- case 'INIT' : return 'Loading\niCal ' ;
57+ case 'INIT' : return 'Please\nSetup ' ;
5858 case 'LOADING' : return 'Loading\niCal' ;
59+ case 'INVALID_URL' : return 'Please\nSetup' ;
5960 case 'LOADED' : return '' ;
6061 default : return 'Error' ;
6162 }
@@ -116,7 +117,7 @@ class TestAction {
116117 const statusText = getStatusText ( calendarCache . status ) ;
117118 action . setTitle ( statusText ) ;
118119 }
119- } , 2000 ) ;
120+ } , 500 ) ;
120121 }
121122
122123 async onWillDisappear ( ev : any ) : Promise < void > {
@@ -247,7 +248,7 @@ describe('BaseAction', () => {
247248
248249 // Simulate cache becoming available
249250 ( calendarCache as any ) . status = 'LOADED' ;
250- vi . advanceTimersByTime ( 2000 ) ;
251+ vi . advanceTimersByTime ( 500 ) ;
251252
252253 expect ( testAction . getInterval ( ) ) . toBeDefined ( ) ;
253254 } ) ;
@@ -399,3 +400,204 @@ describe('Action Settings Integration', () => {
399400 expect ( mockSettings . flashOnMeetingStart ) . toBe ( false ) ;
400401 } ) ;
401402} ) ;
403+
404+ describe ( 'v2.1.0 Feature Tests' , ( ) => {
405+ let testAction : TestAction ;
406+ let mockAction : any ;
407+
408+ beforeEach ( ( ) => {
409+ vi . clearAllMocks ( ) ;
410+ vi . useFakeTimers ( ) ;
411+
412+ // Reset mock settings to defaults
413+ mockSettings . titleDisplayDuration = 15 ;
414+ mockSettings . flashOnMeetingStart = true ;
415+
416+ mockAction = {
417+ setTitle : vi . fn ( ) ,
418+ setImage : vi . fn ( ) ,
419+ showOk : vi . fn ( )
420+ } ;
421+ testAction = new TestAction ( ) ;
422+
423+ // Reset cache
424+ ( calendarCache as any ) . version = 1 ;
425+ ( calendarCache as any ) . status = 'LOADED' ;
426+ ( calendarCache as any ) . events = [ ] ;
427+ } ) ;
428+
429+ afterEach ( ( ) => {
430+ vi . useRealTimers ( ) ;
431+ } ) ;
432+
433+ describe ( 'Title Display Duration' , ( ) => {
434+ it ( 'should support all valid duration values (5, 10, 15, 30 seconds)' , ( ) => {
435+ const validDurations = [ 5 , 10 , 15 , 30 ] ;
436+
437+ for ( const duration of validDurations ) {
438+ mockSettings . titleDisplayDuration = duration ;
439+ expect ( mockSettings . titleDisplayDuration ) . toBe ( duration ) ;
440+ }
441+ } ) ;
442+
443+ it ( 'should default to 15 seconds when not set' , ( ) => {
444+ // Simulate undefined setting
445+ const settings = { titleDisplayDuration : undefined } ;
446+ const displayDuration = settings . titleDisplayDuration ?? 15 ;
447+ expect ( displayDuration ) . toBe ( 15 ) ;
448+ } ) ;
449+
450+ it ( 'should return duration in seconds (not milliseconds)' , ( ) => {
451+ // The getTitleDisplayDuration method should return seconds
452+ // The caller multiplies by 1000 to get milliseconds
453+ mockSettings . titleDisplayDuration = 5 ;
454+
455+ // Simulate what getTitleDisplayDuration returns
456+ const durationInSeconds = mockSettings . titleDisplayDuration ?? 15 ;
457+
458+ // This should be 5 (seconds), not 5000 (milliseconds)
459+ expect ( durationInSeconds ) . toBe ( 5 ) ;
460+ expect ( durationInSeconds ) . toBeLessThan ( 100 ) ; // Sanity check it's not in ms
461+ } ) ;
462+ } ) ;
463+
464+ describe ( 'Flash on Meeting Start' , ( ) => {
465+ it ( 'should be a boolean setting' , ( ) => {
466+ expect ( typeof mockSettings . flashOnMeetingStart ) . toBe ( 'boolean' ) ;
467+ } ) ;
468+
469+ it ( 'should default to false for new installations' , ( ) => {
470+ // Per v2.1.0 change, default should be false
471+ // Simulate undefined setting (new installation)
472+ const settings = { flashOnMeetingStart : undefined } ;
473+ // The default behavior: undefined should be treated as false
474+ const flashEnabled = settings . flashOnMeetingStart === true ;
475+ expect ( flashEnabled ) . toBe ( false ) ;
476+ } ) ;
477+
478+ it ( 'should preserve true when explicitly set' , ( ) => {
479+ const settings = { flashOnMeetingStart : true } ;
480+ const flashEnabled = settings . flashOnMeetingStart === true ;
481+ expect ( flashEnabled ) . toBe ( true ) ;
482+ } ) ;
483+
484+ it ( 'should preserve false when explicitly set' , ( ) => {
485+ const settings = { flashOnMeetingStart : false } ;
486+ const flashEnabled = settings . flashOnMeetingStart === true ;
487+ expect ( flashEnabled ) . toBe ( false ) ;
488+ } ) ;
489+ } ) ;
490+
491+ describe ( 'Startup Race Condition Fix' , ( ) => {
492+ it ( 'should start timer immediately when cache is already LOADED' , async ( ) => {
493+ ( calendarCache as any ) . status = 'LOADED' ;
494+ await testAction . onWillAppear ( { action : mockAction } ) ;
495+
496+ // Should have interval set immediately
497+ expect ( testAction . getInterval ( ) ) . toBeDefined ( ) ;
498+ expect ( testAction . getWaitingInterval ( ) ) . toBeUndefined ( ) ;
499+ } ) ;
500+
501+ it ( 'should start timer immediately when cache status is NO_EVENTS' , async ( ) => {
502+ ( calendarCache as any ) . status = 'NO_EVENTS' ;
503+ await testAction . onWillAppear ( { action : mockAction } ) ;
504+
505+ expect ( testAction . getInterval ( ) ) . toBeDefined ( ) ;
506+ expect ( testAction . getWaitingInterval ( ) ) . toBeUndefined ( ) ;
507+ } ) ;
508+
509+ it ( 'should wait and poll when cache is INIT' , async ( ) => {
510+ ( calendarCache as any ) . status = 'INIT' ;
511+ await testAction . onWillAppear ( { action : mockAction } ) ;
512+
513+ // Should be waiting, not running timer
514+ expect ( testAction . getWaitingInterval ( ) ) . toBeDefined ( ) ;
515+ expect ( testAction . getInterval ( ) ) . toBeUndefined ( ) ;
516+ } ) ;
517+
518+ it ( 'should wait and poll when cache is LOADING' , async ( ) => {
519+ ( calendarCache as any ) . status = 'LOADING' ;
520+ await testAction . onWillAppear ( { action : mockAction } ) ;
521+
522+ expect ( testAction . getWaitingInterval ( ) ) . toBeDefined ( ) ;
523+ expect ( testAction . getInterval ( ) ) . toBeUndefined ( ) ;
524+ } ) ;
525+
526+ it ( 'should show Please Setup for INIT status' , async ( ) => {
527+ ( calendarCache as any ) . status = 'INIT' ;
528+ await testAction . onWillAppear ( { action : mockAction } ) ;
529+
530+ expect ( mockAction . setTitle ) . toHaveBeenCalledWith ( 'Please\nSetup' ) ;
531+ } ) ;
532+
533+ it ( 'should show Loading iCal for LOADING status' , async ( ) => {
534+ ( calendarCache as any ) . status = 'LOADING' ;
535+ await testAction . onWillAppear ( { action : mockAction } ) ;
536+
537+ expect ( mockAction . setTitle ) . toHaveBeenCalledWith ( 'Loading\niCal' ) ;
538+ } ) ;
539+
540+ it ( 'should start timer when cache transitions from LOADING to LOADED' , async ( ) => {
541+ ( calendarCache as any ) . status = 'LOADING' ;
542+ await testAction . onWillAppear ( { action : mockAction } ) ;
543+
544+ // Initially waiting
545+ expect ( testAction . getWaitingInterval ( ) ) . toBeDefined ( ) ;
546+ expect ( testAction . getInterval ( ) ) . toBeUndefined ( ) ;
547+
548+ // Cache becomes ready
549+ ( calendarCache as any ) . status = 'LOADED' ;
550+
551+ // Advance time for polling interval (500ms in the fix)
552+ vi . advanceTimersByTime ( 500 ) ;
553+
554+ // Should now have timer running
555+ expect ( testAction . getInterval ( ) ) . toBeDefined ( ) ;
556+ } ) ;
557+
558+ it ( 'should poll at 500ms intervals (fast startup response)' , async ( ) => {
559+ ( calendarCache as any ) . status = 'LOADING' ;
560+ await testAction . onWillAppear ( { action : mockAction } ) ;
561+
562+ // At 400ms, still waiting
563+ vi . advanceTimersByTime ( 400 ) ;
564+ expect ( testAction . getWaitingInterval ( ) ) . toBeDefined ( ) ;
565+
566+ // Cache becomes ready
567+ ( calendarCache as any ) . status = 'LOADED' ;
568+
569+ // At 500ms (100ms more), should detect and start
570+ vi . advanceTimersByTime ( 100 ) ;
571+ expect ( testAction . getInterval ( ) ) . toBeDefined ( ) ;
572+ } ) ;
573+
574+ it ( 'should clear waiting interval when cache becomes available' , async ( ) => {
575+ ( calendarCache as any ) . status = 'LOADING' ;
576+ await testAction . onWillAppear ( { action : mockAction } ) ;
577+
578+ expect ( testAction . getWaitingInterval ( ) ) . toBeDefined ( ) ;
579+
580+ ( calendarCache as any ) . status = 'LOADED' ;
581+ vi . advanceTimersByTime ( 500 ) ;
582+
583+ // Waiting interval should be cleared
584+ expect ( testAction . getWaitingInterval ( ) ) . toBeUndefined ( ) ;
585+ } ) ;
586+ } ) ;
587+
588+ describe ( 'INIT vs INVALID_URL Status' , ( ) => {
589+ it ( 'should show Please Setup for both INIT and INVALID_URL' , ( ) => {
590+ // Both statuses should show the same message to guide user to setup
591+ const initText = getStatusText ( 'INIT' ) ;
592+ const invalidUrlText = getStatusText ( 'INVALID_URL' ) ;
593+
594+ expect ( initText ) . toBe ( 'Please\nSetup' ) ;
595+ expect ( invalidUrlText ) . toBe ( 'Please\nSetup' ) ;
596+ } ) ;
597+
598+ it ( 'should show Loading iCal only for LOADING status' , ( ) => {
599+ const loadingText = getStatusText ( 'LOADING' ) ;
600+ expect ( loadingText ) . toBe ( 'Loading\niCal' ) ;
601+ } ) ;
602+ } ) ;
603+ } ) ;
0 commit comments