@@ -229,10 +229,12 @@ describe('serviceWorker onInstalled event', () => {
229229 expect ( popCheck ) . toHaveBeenCalledOnce ( ) ;
230230 } ) ;
231231
232- it ( 'checks for pending recovery notification' , async ( ) => {
233- const sessionGetMock = vi . fn ( ) . mockResolvedValue ( {
234- pendingRecoveryNotification : 5 ,
235- } ) ;
232+ // Helper function to reduce test duplication
233+ async function setupRecoveryNotificationTest ( sessionData : {
234+ pendingRecoveryNotification : number ;
235+ lastRecoveryNotifiedAt ?: number ;
236+ } ) {
237+ const sessionGetMock = vi . fn ( ) . mockResolvedValue ( sessionData ) ;
236238 const sessionSetMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
237239 const sessionRemoveMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
238240 const notificationsCreateMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
@@ -243,12 +245,16 @@ describe('serviceWorker onInstalled event', () => {
243245 ( globalThis . chrome . notifications . create as ReturnType < typeof vi . fn > ) = notificationsCreateMock ;
244246
245247 await importServiceWorker ( ) ;
246-
247248 expect ( installedHandler ) . not . toBeNull ( ) ;
248-
249- // Trigger onInstalled event
250249 await installedHandler ! ( ) ;
251250
251+ return { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } ;
252+ }
253+
254+ it ( 'checks for pending recovery notification' , async ( ) => {
255+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
256+ await setupRecoveryNotificationTest ( { pendingRecoveryNotification : 5 } ) ;
257+
252258 // Should check for pending recovery notification
253259 expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
254260
@@ -267,6 +273,158 @@ describe('serviceWorker onInstalled event', () => {
267273 // Should clear pending flag
268274 expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
269275 } ) ;
276+
277+ it ( 'suppresses notification when within 5-minute cooldown' , async ( ) => {
278+ const now = Date . now ( ) ;
279+ const recentNotification = now - ( 3 * 60 * 1000 ) ; // 3 minutes ago (within 5-min cooldown)
280+
281+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
282+ await setupRecoveryNotificationTest ( {
283+ pendingRecoveryNotification : 5 ,
284+ lastRecoveryNotifiedAt : recentNotification ,
285+ } ) ;
286+
287+ // Should check for pending recovery notification
288+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
289+
290+ // Should NOT create notification (within cooldown)
291+ expect ( notificationsCreateMock ) . not . toHaveBeenCalled ( ) ;
292+
293+ // Should NOT update timestamp (notification suppressed)
294+ expect ( sessionSetMock ) . not . toHaveBeenCalled ( ) ;
295+
296+ // Should still clear pending flag
297+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
298+ } ) ;
299+
300+ it ( 'shows notification when cooldown has expired' , async ( ) => {
301+ const now = Date . now ( ) ;
302+ const oldNotification = now - ( 6 * 60 * 1000 ) ; // 6 minutes ago (exceeds 5-min cooldown)
303+
304+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
305+ await setupRecoveryNotificationTest ( {
306+ pendingRecoveryNotification : 3 ,
307+ lastRecoveryNotifiedAt : oldNotification ,
308+ } ) ;
309+
310+ // Should check for pending recovery notification
311+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
312+
313+ // Should create notification (cooldown expired)
314+ expect ( notificationsCreateMock ) . toHaveBeenCalledWith ( 'recovery-notification' , {
315+ type : 'basic' ,
316+ iconUrl : 'assets/icon128.png' ,
317+ title : 'Snooooze Data Recovered' ,
318+ message : 'Recovered 3 snoozed tabs from backup.' ,
319+ priority : 1
320+ } ) ;
321+
322+ // Should update timestamp
323+ expect ( sessionSetMock ) . toHaveBeenCalledWith ( { lastRecoveryNotifiedAt : expect . any ( Number ) } ) ;
324+
325+ // Should clear pending flag
326+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
327+ } ) ;
328+
329+ it ( 'shows notification when lastRecoveryNotifiedAt is undefined (first time)' , async ( ) => {
330+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
331+ await setupRecoveryNotificationTest ( {
332+ pendingRecoveryNotification : 2 ,
333+ // lastRecoveryNotifiedAt is undefined (first time)
334+ } ) ;
335+
336+ // Should check for pending recovery notification
337+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
338+
339+ // Should create notification (first time, no previous notification)
340+ expect ( notificationsCreateMock ) . toHaveBeenCalledWith ( 'recovery-notification' , {
341+ type : 'basic' ,
342+ iconUrl : 'assets/icon128.png' ,
343+ title : 'Snooooze Data Recovered' ,
344+ message : 'Recovered 2 snoozed tabs from backup.' ,
345+ priority : 1
346+ } ) ;
347+
348+ // Should update timestamp
349+ expect ( sessionSetMock ) . toHaveBeenCalledWith ( { lastRecoveryNotifiedAt : expect . any ( Number ) } ) ;
350+
351+ // Should clear pending flag
352+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
353+ } ) ;
354+
355+ it ( 'suppresses notification at exactly 5-minute boundary' , async ( ) => {
356+ const now = Date . now ( ) ;
357+ const NOTIFICATION_COOLDOWN = 5 * 60 * 1000 ;
358+ const exactBoundary = now - NOTIFICATION_COOLDOWN ; // Exactly 5 minutes
359+
360+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
361+ await setupRecoveryNotificationTest ( {
362+ pendingRecoveryNotification : 4 ,
363+ lastRecoveryNotifiedAt : exactBoundary ,
364+ } ) ;
365+
366+ // Should check for pending recovery notification
367+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
368+
369+ // Should NOT create notification (boundary case: condition uses > not >=)
370+ expect ( notificationsCreateMock ) . not . toHaveBeenCalled ( ) ;
371+
372+ // Should NOT update timestamp
373+ expect ( sessionSetMock ) . not . toHaveBeenCalled ( ) ;
374+
375+ // Should still clear pending flag
376+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
377+ } ) ;
378+
379+ it ( 'shows corruption message when zero tabs recovered' , async ( ) => {
380+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
381+ await setupRecoveryNotificationTest ( {
382+ pendingRecoveryNotification : 0 , // Corruption case
383+ } ) ;
384+
385+ // Should check for pending recovery notification
386+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
387+
388+ // Should create notification with corruption message
389+ expect ( notificationsCreateMock ) . toHaveBeenCalledWith ( 'recovery-notification' , {
390+ type : 'basic' ,
391+ iconUrl : 'assets/icon128.png' ,
392+ title : 'Snooooze Data Recovered' ,
393+ message : 'Snoozed tabs data was reset due to corruption.' ,
394+ priority : 1
395+ } ) ;
396+
397+ // Should update timestamp
398+ expect ( sessionSetMock ) . toHaveBeenCalledWith ( { lastRecoveryNotifiedAt : expect . any ( Number ) } ) ;
399+
400+ // Should clear pending flag
401+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
402+ } ) ;
403+
404+ it ( 'uses singular "tab" when recovering one tab' , async ( ) => {
405+ const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } =
406+ await setupRecoveryNotificationTest ( {
407+ pendingRecoveryNotification : 1 , // Singular case
408+ } ) ;
409+
410+ // Should check for pending recovery notification
411+ expect ( sessionGetMock ) . toHaveBeenCalledWith ( [ 'pendingRecoveryNotification' , 'lastRecoveryNotifiedAt' ] ) ;
412+
413+ // Should create notification with singular "tab" (no 's')
414+ expect ( notificationsCreateMock ) . toHaveBeenCalledWith ( 'recovery-notification' , {
415+ type : 'basic' ,
416+ iconUrl : 'assets/icon128.png' ,
417+ title : 'Snooooze Data Recovered' ,
418+ message : 'Recovered 1 snoozed tab from backup.' ,
419+ priority : 1
420+ } ) ;
421+
422+ // Should update timestamp
423+ expect ( sessionSetMock ) . toHaveBeenCalledWith ( { lastRecoveryNotifiedAt : expect . any ( Number ) } ) ;
424+
425+ // Should clear pending flag
426+ expect ( sessionRemoveMock ) . toHaveBeenCalledWith ( 'pendingRecoveryNotification' ) ;
427+ } ) ;
270428} ) ;
271429
272430describe ( 'serviceWorker onStartup event' , ( ) => {
0 commit comments