diff --git a/src/preload/index.test.ts b/src/preload/index.test.ts
index b3b54126b..cf3d3c959 100644
--- a/src/preload/index.test.ts
+++ b/src/preload/index.test.ts
@@ -108,23 +108,18 @@ describe('preload/index', () => {
});
it('app.version returns dev in development', async () => {
- const originalEnv = process.env.NODE_ENV;
- process.env.NODE_ENV = 'development';
+ vi.stubEnv('NODE_ENV', 'development');
const api = getExposedApi();
await expect(api.app.version()).resolves.toBe('dev');
-
- process.env.NODE_ENV = originalEnv;
});
it('app.version prefixes production version', async () => {
- const originalEnv = process.env.NODE_ENV;
- process.env.NODE_ENV = 'production';
+ vi.stubEnv('NODE_ENV', 'production');
invokeMainEventMock.mockResolvedValueOnce('1.2.3');
const api = getExposedApi();
await expect(api.app.version()).resolves.toBe('v1.2.3');
- process.env.NODE_ENV = originalEnv;
});
it('raiseNativeNotification without url calls app.show', async () => {
diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx
index 0cf7bca4c..9b0d08544 100644
--- a/src/renderer/__helpers__/test-utils.tsx
+++ b/src/renderer/__helpers__/test-utils.tsx
@@ -4,7 +4,7 @@ import { MemoryRouter } from 'react-router-dom';
import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
-import { AppContext, type AppContextState } from '../context/App';
+import { AppContext, type AppContextState } from '../context/context';
import { type FiltersStore, useFiltersStore } from '../stores';
export { navigateMock } from './vitest.setup';
diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts
index 0ea8e1574..4589132c4 100644
--- a/src/renderer/__helpers__/vitest.setup.ts
+++ b/src/renderer/__helpers__/vitest.setup.ts
@@ -15,54 +15,82 @@ vi.mock('../utils/core/random', () => ({
randomElement: vi.fn((arr: unknown[]) => arr[0]),
}));
-// Sets timezone to UTC for consistent date/time in tests and snapshots
-process.env.TZ = 'UTC';
+function getRequestTarget(input: RequestInfo | URL): string {
+ if (typeof input === 'string') {
+ return input;
+ }
+
+ if (input instanceof URL) {
+ return input.href;
+ }
+
+ if (typeof Request !== 'undefined' && input instanceof Request) {
+ return input.url;
+ }
+
+ return String(input);
+}
+
+function createGitifyBridgeApi(): Window['gitify'] {
+ return {
+ app: {
+ version: vi.fn().mockResolvedValue('v0.0.1'),
+ hide: vi.fn(),
+ quit: vi.fn(),
+ show: vi.fn(),
+ },
+ twemojiDirectory: vi.fn().mockResolvedValue('/mock/images/assets'),
+ openExternalLink: vi.fn(),
+ decryptValue: vi.fn().mockResolvedValue({ token: 'decrypted' }),
+ encryptValue: vi.fn().mockResolvedValue('encrypted'),
+ platform: {
+ isLinux: vi.fn().mockReturnValue(false),
+ isMacOS: vi.fn().mockReturnValue(true),
+ isWindows: vi.fn().mockReturnValue(false),
+ },
+ zoom: {
+ getLevel: vi.fn(),
+ setLevel: vi.fn(),
+ },
+ tray: {
+ updateColor: vi.fn(),
+ updateTitle: vi.fn(),
+ useAlternateIdleIcon: vi.fn(),
+ useUnreadActiveIcon: vi.fn(),
+ },
+ notificationSoundPath: vi.fn(),
+ onAuthCallback: vi.fn(),
+ onResetApp: vi.fn(),
+ setAutoLaunch: vi.fn(),
+ setKeepWindowOnBlur: vi.fn(),
+ applyKeyboardShortcut: vi.fn().mockResolvedValue({ success: true }),
+ raiseNativeNotification: vi.fn(),
+ };
+}
+
+window.gitify = createGitifyBridgeApi();
/**
* Reset stores
*/
beforeEach(() => {
+ vi.stubEnv('TZ', 'UTC');
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async (input: RequestInfo | URL) => {
+ throw new Error(
+ `Unexpected network request in test: ${getRequestTarget(input)}. Mock the network boundary explicitly.`,
+ );
+ }),
+ );
useFiltersStore.getState().reset();
navigateMock.mockReset();
+ window.gitify = createGitifyBridgeApi();
});
-/**
- * Gitify context bridge API
- */
-window.gitify = {
- app: {
- version: vi.fn().mockResolvedValue('v0.0.1'),
- hide: vi.fn(),
- quit: vi.fn(),
- show: vi.fn(),
- },
- twemojiDirectory: vi.fn().mockResolvedValue('/mock/images/assets'),
- openExternalLink: vi.fn(),
- decryptValue: vi.fn().mockResolvedValue({ token: 'decrypted' }),
- encryptValue: vi.fn().mockResolvedValue('encrypted'),
- platform: {
- isLinux: vi.fn().mockReturnValue(false),
- isMacOS: vi.fn().mockReturnValue(true),
- isWindows: vi.fn().mockReturnValue(false),
- },
- zoom: {
- getLevel: vi.fn(),
- setLevel: vi.fn(),
- },
- tray: {
- updateColor: vi.fn(),
- updateTitle: vi.fn(),
- useAlternateIdleIcon: vi.fn(),
- useUnreadActiveIcon: vi.fn(),
- },
- notificationSoundPath: vi.fn(),
- onAuthCallback: vi.fn(),
- onResetApp: vi.fn(),
- setAutoLaunch: vi.fn(),
- setKeepWindowOnBlur: vi.fn(),
- applyKeyboardShortcut: vi.fn().mockResolvedValue({ success: true }),
- raiseNativeNotification: vi.fn(),
-};
+afterEach(() => {
+ vi.useRealTimers();
+});
// Mock clipboard API
Object.defineProperty(navigator, 'clipboard', {
diff --git a/src/renderer/components/Sidebar.test.tsx b/src/renderer/components/Sidebar.test.tsx
index 56e16f50a..8a13d642b 100644
--- a/src/renderer/components/Sidebar.test.tsx
+++ b/src/renderer/components/Sidebar.test.tsx
@@ -2,7 +2,6 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { navigateMock, renderWithProviders } from '../__helpers__/test-utils';
-import { mockMultipleAccountNotifications } from '../__mocks__/notifications-mocks';
import { mockSettings } from '../__mocks__/state-mocks';
import * as comms from '../utils/system/comms';
@@ -60,7 +59,9 @@ describe('renderer/components/Sidebar.tsx', () => {
it('renders correct icon when there are notifications', () => {
renderWithProviders(, {
- notifications: mockMultipleAccountNotifications,
+ notificationCount: 2,
+ hasNotifications: true,
+ hasUnreadNotifications: false,
});
expect(screen.getByTestId('sidebar-notifications')).toMatchSnapshot();
@@ -132,9 +133,7 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('quick links', () => {
it('opens my github issues page', async () => {
- renderWithProviders(, {
- notifications: mockMultipleAccountNotifications,
- });
+ renderWithProviders();
await userEvent.click(screen.getByTestId('sidebar-my-issues'));
@@ -143,9 +142,7 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('opens my github pull requests page', async () => {
- renderWithProviders(, {
- notifications: mockMultipleAccountNotifications,
- });
+ renderWithProviders();
await userEvent.click(screen.getByTestId('sidebar-my-pull-requests'));
diff --git a/src/renderer/components/settings/SettingsReset.test.tsx b/src/renderer/components/settings/SettingsReset.test.tsx
index 0b8250028..74bc4f12b 100644
--- a/src/renderer/components/settings/SettingsReset.test.tsx
+++ b/src/renderer/components/settings/SettingsReset.test.tsx
@@ -12,7 +12,10 @@ describe('renderer/components/settings/SettingsReset.tsx', () => {
it('should reset default settings when `OK`', async () => {
const rendererLogInfoSpy = vi.spyOn(logger, 'rendererLogInfo').mockImplementation(vi.fn());
- globalThis.confirm = vi.fn(() => true); // always click 'OK'
+ vi.stubGlobal(
+ 'confirm',
+ vi.fn(() => true),
+ ); // always click 'OK'
await act(async () => {
renderWithProviders(, {
@@ -28,7 +31,10 @@ describe('renderer/components/settings/SettingsReset.tsx', () => {
});
it('should skip reset default settings when `cancelled`', async () => {
- globalThis.confirm = vi.fn(() => false); // always click 'cancel'
+ vi.stubGlobal(
+ 'confirm',
+ vi.fn(() => false),
+ ); // always click 'cancel'
await act(async () => {
renderWithProviders(, {
diff --git a/src/renderer/context/App.test.tsx b/src/renderer/context/App.test.tsx
index 7ad8420e6..0a076d5cf 100644
--- a/src/renderer/context/App.test.tsx
+++ b/src/renderer/context/App.test.tsx
@@ -19,7 +19,8 @@ import { getAdapter } from '../utils/forges/registry';
import * as notifications from '../utils/notifications/notifications';
import * as comms from '../utils/system/comms';
import * as tray from '../utils/system/tray';
-import { type AppContextState, AppProvider } from './App';
+import { AppProvider } from './App';
+import { type AppContextState } from './context';
import { defaultSettings } from './defaults';
vi.mock('../hooks/useNotifications');
diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx
index 8c13f0dd3..f9f5ce4ff 100644
--- a/src/renderer/context/App.tsx
+++ b/src/renderer/context/App.tsx
@@ -1,12 +1,4 @@
-import {
- createContext,
- type ReactNode,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react';
+import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTheme } from '@primer/react';
@@ -19,15 +11,12 @@ import { useFiltersStore } from '../stores';
import type {
Account,
- AccountNotifications,
AuthState,
Forge,
- GitifyError,
GitifyNotification,
Hostname,
SettingsState,
SettingsValue,
- Status,
Token,
} from '../types';
import { FetchType } from '../types';
@@ -66,6 +55,7 @@ import {
mapThemeModeToColorScheme,
} from '../utils/ui/theme';
import { zoomLevelToPercentage, zoomPercentageToLevel } from '../utils/ui/zoom';
+import { AppContext, type AppContextState } from './context';
import { defaultAuth, defaultSettings } from './defaults';
/**
@@ -96,47 +86,6 @@ function migrateLegacyAuthState(auth: AuthState): AuthState {
};
}
-export interface AppContextState {
- auth: AuthState;
- isLoggedIn: boolean;
- loginWithDeviceFlowStart: (
- forge: Forge,
- hostname?: Hostname,
- scopes?: string[],
- ) => Promise;
- loginWithDeviceFlowPoll: (forge: Forge, session: DeviceFlowSession) => Promise;
- loginWithDeviceFlowComplete: (forge: Forge, token: Token, hostname: Hostname) => Promise;
- loginWithOAuthApp: (forge: Forge, data: LoginOAuthWebOptions) => Promise;
- loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => Promise;
- logoutFromAccount: (account: Account) => Promise;
-
- status: Status;
- globalError: GitifyError | undefined;
-
- notifications: AccountNotifications[];
- notificationCount: number;
- unreadNotificationCount: number;
- hasNotifications: boolean;
- hasUnreadNotifications: boolean;
-
- fetchNotifications: () => Promise;
- removeAccountNotifications: (account: Account) => Promise;
-
- markNotificationsAsRead: (notifications: GitifyNotification[]) => Promise;
- markNotificationsAsDone: (notifications: GitifyNotification[]) => Promise;
- unsubscribeNotification: (notification: GitifyNotification) => Promise;
-
- settings: SettingsState;
- resetSettings: () => void;
- updateSetting: (name: keyof SettingsState, value: SettingsValue) => void;
-
- /** Shown when the OS could not register the chosen global shortcut. */
- shortcutRegistrationError: string | null;
- clearShortcutRegistrationError: () => void;
-}
-
-export const AppContext = createContext | undefined>(undefined);
-
export const AppProvider = ({ children }: { children: ReactNode }) => {
const existingState = loadState();
diff --git a/src/renderer/context/context.ts b/src/renderer/context/context.ts
new file mode 100644
index 000000000..1ac2b80e7
--- /dev/null
+++ b/src/renderer/context/context.ts
@@ -0,0 +1,60 @@
+import { createContext } from 'react';
+
+import type {
+ Account,
+ AccountNotifications,
+ AuthState,
+ Forge,
+ GitifyError,
+ GitifyNotification,
+ Hostname,
+ SettingsState,
+ SettingsValue,
+ Status,
+ Token,
+} from '../types';
+import type {
+ DeviceFlowSession,
+ LoginOAuthWebOptions,
+ LoginPersonalAccessTokenOptions,
+} from '../utils/auth/types';
+
+export interface AppContextState {
+ auth: AuthState;
+ isLoggedIn: boolean;
+ loginWithDeviceFlowStart: (
+ forge: Forge,
+ hostname?: Hostname,
+ scopes?: string[],
+ ) => Promise;
+ loginWithDeviceFlowPoll: (forge: Forge, session: DeviceFlowSession) => Promise;
+ loginWithDeviceFlowComplete: (forge: Forge, token: Token, hostname: Hostname) => Promise;
+ loginWithOAuthApp: (forge: Forge, data: LoginOAuthWebOptions) => Promise;
+ loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => Promise;
+ logoutFromAccount: (account: Account) => Promise;
+
+ status: Status;
+ globalError: GitifyError | undefined;
+
+ notifications: AccountNotifications[];
+ notificationCount: number;
+ unreadNotificationCount: number;
+ hasNotifications: boolean;
+ hasUnreadNotifications: boolean;
+
+ fetchNotifications: () => Promise;
+ removeAccountNotifications: (account: Account) => Promise;
+
+ markNotificationsAsRead: (notifications: GitifyNotification[]) => Promise;
+ markNotificationsAsDone: (notifications: GitifyNotification[]) => Promise;
+ unsubscribeNotification: (notification: GitifyNotification) => Promise;
+
+ settings: SettingsState;
+ resetSettings: () => void;
+ updateSetting: (name: keyof SettingsState, value: SettingsValue) => void;
+
+ shortcutRegistrationError: string | null;
+ clearShortcutRegistrationError: () => void;
+}
+
+export const AppContext = createContext | undefined>(undefined);
diff --git a/src/renderer/hooks/useAppContext.ts b/src/renderer/hooks/useAppContext.ts
index e85db920a..207aca93e 100644
--- a/src/renderer/hooks/useAppContext.ts
+++ b/src/renderer/hooks/useAppContext.ts
@@ -1,6 +1,6 @@
import { useContext } from 'react';
-import { AppContext, type AppContextState } from '../context/App';
+import { AppContext, type AppContextState } from '../context/context';
/**
* Custom hook that provides type-safe access to AppContext.
diff --git a/src/renderer/routes/LoginWithDeviceFlow.test.tsx b/src/renderer/routes/LoginWithDeviceFlow.test.tsx
index 48f8b0a71..435f55d24 100644
--- a/src/renderer/routes/LoginWithDeviceFlow.test.tsx
+++ b/src/renderer/routes/LoginWithDeviceFlow.test.tsx
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
import { navigateMock, renderWithProviders } from '../__helpers__/test-utils';
+import * as logger from '../utils/core/logger';
import * as comms from '../utils/system/comms';
import { LoginWithDeviceFlowRoute } from './LoginWithDeviceFlow';
@@ -117,6 +118,7 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
});
it('should handle device flow errors during initialization', async () => {
+ const rendererLogErrorSpy = vi.spyOn(logger, 'rendererLogError').mockImplementation(vi.fn());
const loginWithDeviceFlowStartMock = vi.fn().mockRejectedValueOnce(new Error('Network error'));
renderWithProviders(, {
@@ -126,6 +128,7 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
await userEvent.click(screen.getByTestId('device-scope-full'));
await screen.findByText(/Failed to start authentication/);
+ expect(rendererLogErrorSpy).toHaveBeenCalledTimes(1);
});
it('should navigate back on cancel from scope choice', async () => {
diff --git a/src/renderer/utils/forges/gitea/client.test.ts b/src/renderer/utils/forges/gitea/client.test.ts
index 0297cfa6f..896380e7e 100644
--- a/src/renderer/utils/forges/gitea/client.test.ts
+++ b/src/renderer/utils/forges/gitea/client.test.ts
@@ -12,12 +12,7 @@ import {
} from './client';
describe('renderer/utils/forges/gitea/client.ts', () => {
- const fetchSpy = vi.spyOn(globalThis, 'fetch');
-
- beforeEach(() => {
- fetchSpy.mockReset();
- vi.spyOn(comms, 'decryptValue').mockResolvedValue({ token: 'decrypted' });
- });
+ const fetchMock = () => vi.mocked(globalThis.fetch);
function jsonResponse(body: T, init: ResponseInit = { status: 200 }) {
return new Response(JSON.stringify(body), {
@@ -26,6 +21,11 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
});
}
+ beforeEach(() => {
+ fetchMock().mockReset();
+ vi.spyOn(comms, 'decryptValue').mockResolvedValue({ token: 'decrypted' });
+ });
+
describe('getGiteaApiBaseUrl', () => {
it('builds https api v1 base', () => {
const url = getGiteaApiBaseUrl('gitea.example.com' as Hostname);
@@ -35,7 +35,7 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
describe('listGiteaNotifications', () => {
it('fetches a single page when fetchAllNotifications is false', async () => {
- fetchSpy.mockResolvedValueOnce(jsonResponse([{ id: 1 }]));
+ fetchMock().mockResolvedValueOnce(jsonResponse([{ id: 1 }]));
const result = await listGiteaNotifications(mockGiteaAccount, {
fetchAllNotifications: false,
@@ -43,8 +43,8 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
} as SettingsState);
expect(result).toEqual([{ id: 1 }]);
- expect(fetchSpy).toHaveBeenCalledTimes(1);
- const calledUrl = fetchSpy.mock.calls[0][0] as string;
+ expect(fetchMock()).toHaveBeenCalledTimes(1);
+ const calledUrl = fetchMock().mock.calls[0][0] as string;
expect(calledUrl).toContain('https://gitea.example.com/api/v1/');
expect(calledUrl).toContain('status-types=unread');
expect(calledUrl).toContain('page=1');
@@ -52,20 +52,20 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
});
it('includes read status when fetchReadNotifications is true', async () => {
- fetchSpy.mockResolvedValueOnce(jsonResponse([]));
+ fetchMock().mockResolvedValueOnce(jsonResponse([]));
await listGiteaNotifications(mockGiteaAccount, {
fetchAllNotifications: false,
fetchReadNotifications: true,
} as SettingsState);
- const calledUrl = fetchSpy.mock.calls[0][0] as string;
+ const calledUrl = fetchMock().mock.calls[0][0] as string;
expect(calledUrl).toContain('status-types=unread');
expect(calledUrl).toContain('status-types=read');
});
it('paginates until an empty page is returned', async () => {
- fetchSpy
+ fetchMock()
.mockResolvedValueOnce(jsonResponse(Array.from({ length: 100 }, (_, i) => ({ id: i }))))
.mockResolvedValueOnce(jsonResponse([{ id: 100 }]))
.mockResolvedValueOnce(jsonResponse([]));
@@ -76,11 +76,11 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
} as SettingsState);
expect(result).toHaveLength(101);
- expect(fetchSpy).toHaveBeenCalledTimes(2);
+ expect(fetchMock()).toHaveBeenCalledTimes(2);
});
it('throws on a non-ok status without echoing the response body', async () => {
- fetchSpy.mockResolvedValueOnce(
+ fetchMock().mockResolvedValue(
new Response('Authorization: token leaked-pat', {
status: 403,
statusText: 'Forbidden',
@@ -106,22 +106,22 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
describe('fetchGiteaAuthenticatedUser', () => {
it('returns the user payload', async () => {
- fetchSpy.mockResolvedValueOnce(jsonResponse({ id: 7, login: 'octocat' }));
+ fetchMock().mockResolvedValueOnce(jsonResponse({ id: 7, login: 'octocat' }));
const result = await fetchGiteaAuthenticatedUser(mockGiteaAccount);
expect(result).toEqual({ id: 7, login: 'octocat' });
- expect(fetchSpy.mock.calls[0][0]).toContain('/api/v1/user');
+ expect(fetchMock().mock.calls[0][0]).toContain('/api/v1/user');
});
});
describe('patchGiteaNotificationThread', () => {
it('sends a PATCH with to-status query and resolves on 204', async () => {
- fetchSpy.mockResolvedValueOnce(new Response(null, { status: 204 }));
+ fetchMock().mockResolvedValueOnce(new Response(null, { status: 204 }));
await patchGiteaNotificationThread(mockGiteaAccount, '42', 'read');
- const [url, init] = fetchSpy.mock.calls[0];
+ const [url, init] = fetchMock().mock.calls[0];
expect(url).toContain('/notifications/threads/42?to-status=read');
expect((init as RequestInit).method).toBe('PATCH');
});
@@ -129,7 +129,7 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
describe('giteaGetJson', () => {
it('GETs the supplied URL with auth headers and parses JSON', async () => {
- fetchSpy.mockResolvedValueOnce(jsonResponse({ html_url: 'x' }));
+ fetchMock().mockResolvedValueOnce(jsonResponse({ html_url: 'x' }));
const result = await giteaGetJson<{ html_url: string }>(
mockGiteaAccount,
@@ -137,12 +137,15 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
);
expect(result).toEqual({ html_url: 'x' });
- const headers = (fetchSpy.mock.calls[0][1] as RequestInit).headers as Record;
+ const headers = (fetchMock().mock.calls[0][1] as RequestInit).headers as Record<
+ string,
+ string
+ >;
expect(headers.Authorization).toBe('token decrypted');
});
it('throws on a non-ok response without echoing the body', async () => {
- fetchSpy.mockResolvedValueOnce(
+ fetchMock().mockResolvedValueOnce(
new Response('echoed Authorization: token leaked-pat', {
status: 500,
statusText: 'Server Error',
@@ -158,21 +161,21 @@ describe('renderer/utils/forges/gitea/client.ts', () => {
await expect(giteaGetJson(mockGiteaAccount, 'https://attacker.com/api/v1/x')).rejects.toThrow(
/cross-origin Gitea URL/,
);
- expect(fetchSpy).not.toHaveBeenCalled();
+ expect(fetchMock()).not.toHaveBeenCalled();
});
it('refuses non-https URLs without sending a request', async () => {
await expect(giteaGetJson(mockGiteaAccount, 'http://gitea.example.com/x')).rejects.toThrow(
/cross-origin Gitea URL/,
);
- expect(fetchSpy).not.toHaveBeenCalled();
+ expect(fetchMock()).not.toHaveBeenCalled();
});
it('refuses malformed URLs without sending a request', async () => {
await expect(giteaGetJson(mockGiteaAccount, 'not-a-url')).rejects.toThrow(
/malformed Gitea URL/,
);
- expect(fetchSpy).not.toHaveBeenCalled();
+ expect(fetchMock()).not.toHaveBeenCalled();
});
});
});
diff --git a/src/renderer/utils/forges/github/client.test.ts b/src/renderer/utils/forges/github/client.test.ts
index 2b74f0f7b..0c795e8a3 100644
--- a/src/renderer/utils/forges/github/client.test.ts
+++ b/src/renderer/utils/forges/github/client.test.ts
@@ -36,6 +36,15 @@ import type { OctokitClient } from './octokit';
import * as octokitModule from './octokit';
import * as apiRequests from './request';
+vi.mock('./request', async () => {
+ const actual = await vi.importActual('./request');
+ return {
+ ...actual,
+ performGraphQLRequest: vi.fn(),
+ performGraphQLRequestString: vi.fn(),
+ };
+});
+
const mockThreadId = '1234';
describe('renderer/utils/forges/github/client.ts', () => {
@@ -59,6 +68,9 @@ describe('renderer/utils/forges/github/client.ts', () => {
const createOctokitClientUncachedSpy = vi.spyOn(octokitModule, 'createOctokitClientUncached');
beforeEach(() => {
+ vi.mocked(apiRequests.performGraphQLRequest).mockReset();
+ vi.mocked(apiRequests.performGraphQLRequestString).mockReset();
+
// Mock createOctokitClient to return our mock
createOctokitClientSpy.mockResolvedValue(mockOctokit as unknown as OctokitClient);
createOctokitClientUncachedSpy.mockResolvedValue(mockOctokit as unknown as OctokitClient);
@@ -270,7 +282,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
});
it('fetchDiscussionByNumber calls performGraphQLRequest with correct args', async () => {
- const performGraphQLRequestSpy = vi.spyOn(apiRequests, 'performGraphQLRequest');
+ const performGraphQLRequestSpy = vi.mocked(apiRequests.performGraphQLRequest);
const mockNotification = mockPartialGitifyNotification({
title: 'Some discussion',
@@ -298,7 +310,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
});
it('fetchIssueByNumber calls performGraphQLRequest with correct args', async () => {
- const performGraphQLRequestSpy = vi.spyOn(apiRequests, 'performGraphQLRequest');
+ const performGraphQLRequestSpy = vi.mocked(apiRequests.performGraphQLRequest);
const mockNotification = mockPartialGitifyNotification({
title: 'Some issue',
@@ -324,7 +336,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
});
it('fetchPullByNumber calls performGraphQLRequest with correct args', async () => {
- const performGraphQLRequestSpy = vi.spyOn(apiRequests, 'performGraphQLRequest');
+ const performGraphQLRequestSpy = vi.mocked(apiRequests.performGraphQLRequest);
const mockNotification = mockPartialGitifyNotification({
title: 'Some pull request',
@@ -355,7 +367,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
describe('fetchNotificationDetailsForList', () => {
it('fetchNotificationDetailsForList returns empty map if no notifications', async () => {
- const performGraphQLRequestStringSpy = vi.spyOn(apiRequests, 'performGraphQLRequestString');
+ const performGraphQLRequestStringSpy = vi.mocked(apiRequests.performGraphQLRequestString);
const mockNotification = mockPartialGitifyNotification({
title: 'Some commit',
@@ -371,7 +383,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
});
it('fetchNotificationDetailsForList returns empty map if no supported notifications', async () => {
- const performGraphQLRequestStringSpy = vi.spyOn(apiRequests, 'performGraphQLRequestString');
+ const performGraphQLRequestStringSpy = vi.mocked(apiRequests.performGraphQLRequestString);
performGraphQLRequestStringSpy.mockResolvedValue({} as ExecutionResult);
@@ -381,7 +393,7 @@ describe('renderer/utils/forges/github/client.ts', () => {
});
it('fetchNotificationDetailsForList returns empty map if no notifications', async () => {
- const performGraphQLRequestStringSpy = vi.spyOn(apiRequests, 'performGraphQLRequestString');
+ const performGraphQLRequestStringSpy = vi.mocked(apiRequests.performGraphQLRequestString);
performGraphQLRequestStringSpy.mockResolvedValue({
data: {},
diff --git a/src/renderer/utils/forges/github/enrich.test.ts b/src/renderer/utils/forges/github/enrich.test.ts
index 8c21d08dc..7f468c0f9 100644
--- a/src/renderer/utils/forges/github/enrich.test.ts
+++ b/src/renderer/utils/forges/github/enrich.test.ts
@@ -7,6 +7,15 @@ import * as logger from '../../core/logger';
import * as client from './client';
import { enrichGitHubNotifications } from './enrich';
+vi.mock('./client', async () => {
+ const actual = await vi.importActual('./client');
+ return {
+ ...actual,
+ fetchNotificationDetailsForList: vi.fn(),
+ fetchIssueByNumber: vi.fn(),
+ };
+});
+
describe('renderer/utils/forges/github/enrich.ts', () => {
it('logs and continues when a per-notification handler throws', async () => {
const rendererLogErrorSpy = vi.spyOn(logger, 'rendererLogError').mockImplementation(vi.fn());
@@ -14,7 +23,7 @@ describe('renderer/utils/forges/github/enrich.ts', () => {
// No batched fragments — each handler will fall back to its single-fetch
// path, which we make fail.
- vi.spyOn(client, 'fetchNotificationDetailsForList').mockResolvedValue(new Map());
+ vi.mocked(client.fetchNotificationDetailsForList).mockResolvedValue(new Map());
const mockError = new Error('Test error');
const mockNotification = mockPartialGitifyNotification({
@@ -34,7 +43,7 @@ describe('renderer/utils/forges/github/enrich.ts', () => {
};
mockNotification.repository = mockRepository;
- vi.spyOn(client, 'fetchIssueByNumber').mockRejectedValue(mockError);
+ vi.mocked(client.fetchIssueByNumber).mockRejectedValue(mockError);
const [result] = await enrichGitHubNotifications([mockNotification], mockSettings);
diff --git a/src/renderer/utils/forges/github/enrich.ts b/src/renderer/utils/forges/github/enrich.ts
index 89b579b03..babfd57c6 100644
--- a/src/renderer/utils/forges/github/enrich.ts
+++ b/src/renderer/utils/forges/github/enrich.ts
@@ -38,12 +38,15 @@ async function fetchInBatches(
notifications: RawGitifyNotification[],
): Promise