diff --git a/src/__helpers__/restore-test-spies.setup.ts b/src/__helpers__/restore-test-spies.setup.ts new file mode 100644 index 000000000..717f50923 --- /dev/null +++ b/src/__helpers__/restore-test-spies.setup.ts @@ -0,0 +1,45 @@ +import { afterEach, beforeEach, type MockInstance, vi } from 'vitest'; + +/** + * Restore spies created inside a test (or its hooks) after each test. + * + * The suite intentionally keeps some spies at module scope (inside `describe` + * blocks) so every test in the file reuses them; a global `restoreMocks` would + * tear those down after the first test. Track spies instead: module-scope spies + * are created before the first `beforeEach` and are forgotten by the reset, + * while spies created during a test are restored afterwards. + */ +const testSpies: MockInstance[] = []; +const persistentSpies = new Set(); +let firstTest = true; +const originalSpyOn = vi.spyOn.bind(vi) as (...args: unknown[]) => MockInstance; + +vi.spyOn = ((...args: unknown[]) => { + const spy = originalSpyOn(...args); + + if (!persistentSpies.has(spy)) { + testSpies.push(spy); + } + + return spy; +}) as unknown as typeof vi.spyOn; + +beforeEach(() => { + if (firstTest) { + firstTest = false; + + // Anything already spied before the first test belongs to the file, not the + // test. Keep it alive for every test in the file. + for (const spy of testSpies) { + persistentSpies.add(spy); + } + } + + testSpies.length = 0; +}); + +afterEach(() => { + for (const spy of testSpies.splice(0)) { + spy.mockRestore(); + } +}); diff --git a/src/renderer/hooks/notifications/useNotificationActions.test.tsx b/src/renderer/hooks/notifications/useNotificationActions.test.tsx index 4acf3f5ec..2581a6faa 100644 --- a/src/renderer/hooks/notifications/useNotificationActions.test.tsx +++ b/src/renderer/hooks/notifications/useNotificationActions.test.tsx @@ -57,7 +57,6 @@ describe('renderer/hooks/notifications/useNotificationActions.ts', () => { beforeEach(() => { getAllNotificationsMock.mockReset(); useAccountsStore.setState({ accounts: [mockGitHubCloudAccount] }); - mockGitifyNotification.unread = true; }); it('exposes the notification actions without fetching or creating a query', () => { diff --git a/src/renderer/hooks/useNotifications.test.tsx b/src/renderer/hooks/useNotifications.test.tsx index d722818e2..e050fad2b 100644 --- a/src/renderer/hooks/useNotifications.test.tsx +++ b/src/renderer/hooks/useNotifications.test.tsx @@ -93,14 +93,13 @@ describe('renderer/hooks/useNotifications.ts', () => { raiseSoundNotificationSpy.mockClear(); raiseNativeNotificationSpy.mockClear(); getAllNotificationsMock.mockReset(); + getAllNotificationsMock.mockResolvedValue([]); refreshEnrichmentCacheMock.mockReset(); + refreshEnrichmentCacheMock.mockReturnValue(false); clearServerPollIntervals(); useAccountsStore.setState({ accounts: [mockGitHubCloudAccount] }); useNotificationActionFailuresStore.getState().reset(); - - // Reset mock notification state between tests since it's mutated - mockGitifyNotification.unread = true; }); describe('fetching notifications', () => { diff --git a/vitest.config.mts b/vitest.config.mts index bfbe7be11..bd4e45235 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -18,7 +18,7 @@ export default defineConfig({ test: { globals: true, pool: 'vmThreads', - isolate: false, + isolate: true, clearMocks: true, unstubEnvs: true, unstubGlobals: true, @@ -56,6 +56,7 @@ export default defineConfig({ exclude: [...defaultExclude, '**/*.visual.test.tsx'], setupFiles: [ './src/__helpers__/electron-log.setup.ts', + './src/__helpers__/restore-test-spies.setup.ts', './src/renderer/__helpers__/vitest.setup.ts', ], }, @@ -67,7 +68,10 @@ export default defineConfig({ name: 'node [main, shared]', environment: 'node', include: ['src/shared/**/*.test.{ts,tsx}', 'src/main/**/*.test.{ts,tsx}'], - setupFiles: ['./src/__helpers__/electron-log.setup.ts'], + setupFiles: [ + './src/__helpers__/electron-log.setup.ts', + './src/__helpers__/restore-test-spies.setup.ts', + ], }, }, {