-
Notifications
You must be signed in to change notification settings - Fork 0
test: add recovery notification cooldown coverage #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ad14668
61f1734
517b995
d77cfa2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,10 +229,12 @@ describe('serviceWorker onInstalled event', () => { | |
| expect(popCheck).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it('checks for pending recovery notification', async () => { | ||
| const sessionGetMock = vi.fn().mockResolvedValue({ | ||
| pendingRecoveryNotification: 5, | ||
| }); | ||
| // Helper function to reduce test duplication | ||
| async function setupRecoveryNotificationTest(sessionData: { | ||
| pendingRecoveryNotification: number; | ||
| lastRecoveryNotifiedAt?: number; | ||
| }) { | ||
| const sessionGetMock = vi.fn().mockResolvedValue(sessionData); | ||
| const sessionSetMock = vi.fn().mockResolvedValue(undefined); | ||
| const sessionRemoveMock = vi.fn().mockResolvedValue(undefined); | ||
| const notificationsCreateMock = vi.fn().mockResolvedValue(undefined); | ||
|
|
@@ -243,12 +245,16 @@ describe('serviceWorker onInstalled event', () => { | |
| (globalThis.chrome.notifications.create as ReturnType<typeof vi.fn>) = notificationsCreateMock; | ||
|
|
||
| await importServiceWorker(); | ||
|
|
||
| expect(installedHandler).not.toBeNull(); | ||
|
|
||
| // Trigger onInstalled event | ||
| await installedHandler!(); | ||
|
|
||
| return { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock }; | ||
| } | ||
|
|
||
| it('checks for pending recovery notification', async () => { | ||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ pendingRecoveryNotification: 5 }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
|
||
|
|
@@ -267,6 +273,158 @@ describe('serviceWorker onInstalled event', () => { | |
| // Should clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
|
||
| it('suppresses notification when within 5-minute cooldown', async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling test: The implementation has a try-catch block (lines 89-124 in serviceWorker.ts) that catches errors and logs them with
The catch block prevents crashes, but tests should verify:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test naming clarity: Consider renaming to
Starting with "suppresses" vs "shows no" is subtle, but "shows no" better mirrors the affirmative "shows notification" pattern.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test organization observation: The three new cooldown tests are well-structured and test the main scenarios. However, they're only added to the The Consider either:
This would ensure both entry points have documented test coverage for the cooldown feature. |
||
| const now = Date.now(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: Redundant Date.now() calls Each test calls Recommendation: Use a fixed timestamp constant: const FIXED_NOW = 1234567890000;
const recentNotification = FIXED_NOW - (3 * 60 * 1000);Or leverage the fake timers by setting a base time: beforeEach(() => {
vi.setSystemTime(new Date('2024-01-01T12:00:00Z'));
});Impact: Minor performance gain, but improves test determinism and clarity. |
||
| const recentNotification = now - (3 * 60 * 1000); // 3 minutes ago (within 5-min cooldown) | ||
|
|
||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 5, | ||
| lastRecoveryNotifiedAt: recentNotification, | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test Assertion: Unnecessary Check This assertion is redundant since the test in lines 232-269 already validates that Consider removing this line to keep the test focused on its unique behavior (suppression), or add a comment explaining why this verification is important for this specific test case. |
||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
|
||
| // Should NOT create notification (within cooldown) | ||
| expect(notificationsCreateMock).not.toHaveBeenCalled(); | ||
|
|
||
| // Should NOT update timestamp (notification suppressed) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation accuracy: The comment says "Should NOT update timestamp (notification suppressed)" but this deserves additional context. Consider: // Should NOT update timestamp (notification suppressed, so lastRecoveryNotifiedAt unchanged)This clarifies that we're specifically NOT updating |
||
| expect(sessionSetMock).not.toHaveBeenCalled(); | ||
|
|
||
| // Should still clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security Validation - Proper Cleanup: Excellent test coverage here. This assertion confirms that
This is a good security practice for state management. |
||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing boundary condition test: This test uses 3 minutes (well within cooldown), but there's no test for the exact boundary at 5 minutes. According to the implementation logic Consider adding a test case: const exactBoundary = now - (5 * 60 * 1000); // Exactly 5 minutes - should suppress |
||
|
|
||
| it('shows notification when cooldown has expired', async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test naming precision: The test name could be more precise about what "cooldown has expired" means. Consider: it('shows notification when 5-minute cooldown has expired', async () => {This explicitly mentions the 5-minute threshold, matching the first test which also mentions "5-minute cooldown". |
||
| const now = Date.now(); | ||
| const oldNotification = now - (6 * 60 * 1000); // 6 minutes ago (exceeds 5-min cooldown) | ||
|
|
||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 3, | ||
| lastRecoveryNotifiedAt: oldNotification, | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test Assertion: Redundant Check Similar to line 295, this assertion duplicates what's already tested in the basic test case (lines 232-269). Consider removing to reduce noise and keep the test focused on the cooldown expiration behavior. |
||
|
|
||
| // Should create notification (cooldown expired) | ||
| expect(notificationsCreateMock).toHaveBeenCalledWith('recovery-notification', { | ||
| type: 'basic', | ||
| iconUrl: 'assets/icon128.png', | ||
| title: 'Snooooze Data Recovered', | ||
| message: 'Recovered 3 snoozed tabs from backup.', | ||
| priority: 1 | ||
| }); | ||
|
Comment on lines
+314
to
+320
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: Verbose object assertion The full object literal comparison forces the test framework to deep-compare every property. While necessary for correctness, this is repeated across 3 tests (lines 256-262, 336-342, 376-382) with nearly identical content. Recommendation: Extract expected notification object to a helper function: const expectRecoveryNotification = (count: number) => ({
type: 'basic',
iconUrl: 'assets/icon128.png',
title: 'Snooooze Data Recovered',
message: `Recovered ${count} snoozed tabs from backup.`,
priority: 1
});Then use: expect(notificationsCreateMock).toHaveBeenCalledWith(
'recovery-notification',
expectRecoveryNotification(3)
);Impact: Cleaner test code, slightly faster comparison, easier maintenance. |
||
|
|
||
| // Should update timestamp | ||
| expect(sessionSetMock).toHaveBeenCalledWith({ lastRecoveryNotifiedAt: expect.any(Number) }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test Robustness: Timestamp Validation Could Be Stricter The assertion expect(sessionSetMock).toHaveBeenCalledWith({
lastRecoveryNotifiedAt: expect.any(Number)
});
// Validate the timestamp is close to current time
const [[callArg]] = sessionSetMock.mock.calls;
expect(callArg.lastRecoveryNotifiedAt).toBeGreaterThan(oldNotification);
expect(callArg.lastRecoveryNotifiedAt).toBeLessThanOrEqual(Date.now());This ensures the timestamp is actually being set to a current/valid value rather than any arbitrary number.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance: expect.any(Number) runtime overhead Using Recommendation: Use exact value matching with fake timers: // In beforeEach:
vi.setSystemTime(new Date('2024-01-01T12:00:00Z'));
// In test:
expect(sessionSetMock).toHaveBeenCalledWith({
lastRecoveryNotifiedAt: new Date('2024-01-01T12:00:00Z').getTime()
});Impact: Eliminates type checking overhead, more precise assertions.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weak timestamp assertion: Using const timestampCall = sessionSetMock.mock.calls[0][0];
expect(timestampCall.lastRecoveryNotifiedAt).toBeGreaterThanOrEqual(now);
expect(timestampCall.lastRecoveryNotifiedAt).toBeLessThanOrEqual(Date.now());This ensures the timestamp is:
|
||
|
|
||
| // Should clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing boundary condition test: Similar to the suppression test, this uses 6 minutes (safely expired), but there's no test for the exact boundary. Consider testing the edge case at exactly 5 minutes + 1ms to verify the const justExpired = now - (5 * 60 * 1000 + 1); // 5 minutes + 1ms - should show |
||
|
|
||
| it('shows notification when lastRecoveryNotifiedAt is undefined (first time)', async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test description vs PR description mismatch: The PR description says "First-time notification" but the test name says "when lastRecoveryNotifiedAt is undefined (first time)". While technically accurate, this test name conflates two concepts:
Consider whether this is truly the "first time" scenario, or simply the "no previous notification timestamp" scenario. These could be different if If this is definitively the first-time scenario, consider renaming to: it('shows notification on first recovery (no previous timestamp)', async () => {If |
||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 2, | ||
| // lastRecoveryNotifiedAt is undefined (first time) | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
|
||
| // Should create notification (first time, no previous notification) | ||
| expect(notificationsCreateMock).toHaveBeenCalledWith('recovery-notification', { | ||
| type: 'basic', | ||
| iconUrl: 'assets/icon128.png', | ||
| title: 'Snooooze Data Recovered', | ||
| message: 'Recovered 2 snoozed tabs from backup.', | ||
| priority: 1 | ||
| }); | ||
|
|
||
| // Should update timestamp | ||
| expect(sessionSetMock).toHaveBeenCalledWith({ lastRecoveryNotifiedAt: expect.any(Number) }); | ||
|
|
||
| // Should clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
|
||
| it('suppresses notification at exactly 5-minute boundary', async () => { | ||
| const now = Date.now(); | ||
| const NOTIFICATION_COOLDOWN = 5 * 60 * 1000; | ||
| const exactBoundary = now - NOTIFICATION_COOLDOWN; // Exactly 5 minutes | ||
|
|
||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 4, | ||
| lastRecoveryNotifiedAt: exactBoundary, | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test Assertion: Redundant Check This is the third test with this identical assertion. Since you're testing different cooldown scenarios, the |
||
|
|
||
| // Should NOT create notification (boundary case: condition uses > not >=) | ||
| expect(notificationsCreateMock).not.toHaveBeenCalled(); | ||
|
|
||
| // Should NOT update timestamp | ||
| expect(sessionSetMock).not.toHaveBeenCalled(); | ||
|
|
||
| // Should still clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
|
||
| it('shows corruption message when zero tabs recovered', async () => { | ||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 0, // Corruption case | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
|
||
| // Should create notification with corruption message | ||
| expect(notificationsCreateMock).toHaveBeenCalledWith('recovery-notification', { | ||
| type: 'basic', | ||
| iconUrl: 'assets/icon128.png', | ||
| title: 'Snooooze Data Recovered', | ||
| message: 'Snoozed tabs data was reset due to corruption.', | ||
| priority: 1 | ||
| }); | ||
|
|
||
| // Should update timestamp | ||
| expect(sessionSetMock).toHaveBeenCalledWith({ lastRecoveryNotifiedAt: expect.any(Number) }); | ||
|
|
||
| // Should clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
|
||
| it('uses singular "tab" when recovering one tab', async () => { | ||
| const { sessionGetMock, sessionSetMock, sessionRemoveMock, notificationsCreateMock } = | ||
| await setupRecoveryNotificationTest({ | ||
| pendingRecoveryNotification: 1, // Singular case | ||
| }); | ||
|
|
||
| // Should check for pending recovery notification | ||
| expect(sessionGetMock).toHaveBeenCalledWith(['pendingRecoveryNotification', 'lastRecoveryNotifiedAt']); | ||
|
|
||
| // Should create notification with singular "tab" (no 's') | ||
| expect(notificationsCreateMock).toHaveBeenCalledWith('recovery-notification', { | ||
| type: 'basic', | ||
| iconUrl: 'assets/icon128.png', | ||
| title: 'Snooooze Data Recovered', | ||
| message: 'Recovered 1 snoozed tab from backup.', | ||
| priority: 1 | ||
| }); | ||
|
|
||
| // Should update timestamp | ||
| expect(sessionSetMock).toHaveBeenCalledWith({ lastRecoveryNotifiedAt: expect.any(Number) }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weak timestamp assertion: Same issue as the other tests - |
||
|
|
||
| // Should clear pending flag | ||
| expect(sessionRemoveMock).toHaveBeenCalledWith('pendingRecoveryNotification'); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing test coverage for edge cases:
|
||
| }); | ||
|
|
||
| describe('serviceWorker onStartup event', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test Coverage: Missing Edge Cases
Good coverage of the main cooldown scenarios, but consider adding tests for these edge cases:
These edge cases would make the test suite more robust.