From de87f0cf5ba9cf51b17bb26d81c67f54ad732e75 Mon Sep 17 00:00:00 2001
From: Adam Setch
Date: Sun, 16 Nov 2025 15:20:49 -0500
Subject: [PATCH 01/17] test: refactor render with app context
Signed-off-by: Adam Setch
---
src/renderer/__helpers__/test-utils.tsx | 140 +
src/renderer/components/AllRead.test.tsx | 40 +-
src/renderer/components/Oops.test.tsx | 11 +-
src/renderer/components/Sidebar.test.tsx | 262 +-
.../__snapshots__/AllRead.test.tsx.snap | 276 +-
.../__snapshots__/Oops.test.tsx.snap | 316 +-
.../__snapshots__/Sidebar.test.tsx.snap | 1936 ++---
.../avatars/AvatarWithFallback.test.tsx | 17 +-
.../AvatarWithFallback.test.tsx.snap | 500 +-
.../components/fields/Checkbox.test.tsx | 21 +-
.../components/fields/FieldLabel.test.tsx | 5 +-
.../components/fields/RadioGroup.test.tsx | 7 +-
.../components/fields/Tooltip.test.tsx | 38 +-
.../__snapshots__/Checkbox.test.tsx.snap | 770 +-
.../__snapshots__/FieldLabel.test.tsx.snap | 42 +-
.../__snapshots__/RadioGroup.test.tsx.snap | 492 +-
.../components/filters/FilterSection.test.tsx | 126 +-
.../components/filters/ReasonFilter.test.tsx | 18 +-
...uiresDetailedNotificationsWarning.test.tsx | 18 +-
.../components/filters/SearchFilter.test.tsx | 72 +-
.../filters/SearchFilterSuggestions.test.tsx | 74 +-
.../components/filters/StateFilter.test.tsx | 23 +-
.../filters/SubjectTypeFilter.test.tsx | 21 +-
.../filters/UserTypeFilter.test.tsx | 23 +-
.../__snapshots__/FilterSection.test.tsx.snap | 2012 ++---
.../__snapshots__/ReasonFilter.test.tsx.snap | 3352 ++++----
...DetailedNotificationsWarning.test.tsx.snap | 62 +-
.../SearchFilterSuggestions.test.tsx.snap | 846 +-
.../__snapshots__/StateFilter.test.tsx.snap | 1106 +--
.../SubjectTypeFilter.test.tsx.snap | 1330 +--
.../UserTypeFilter.test.tsx.snap | 682 +-
.../components/icons/LogoIcon.test.tsx | 15 +-
.../__snapshots__/LogoIcon.test.tsx.snap | 930 ++-
.../components/layout/AppLayout.test.tsx | 20 +-
.../components/layout/Centered.test.tsx | 11 +-
.../components/layout/Contents.test.tsx | 5 +-
.../components/layout/EmojiSplash.test.tsx | 13 +-
src/renderer/components/layout/Page.test.tsx | 5 +-
.../__snapshots__/Centered.test.tsx.snap | 100 +-
.../__snapshots__/Contents.test.tsx.snap | 30 +-
.../__snapshots__/EmojiSplash.test.tsx.snap | 296 +-
.../layout/__snapshots__/Page.test.tsx.snap | 34 +-
.../components/metrics/MetricGroup.test.tsx | 163 +-
.../components/metrics/MetricPill.test.tsx | 7 +-
.../__snapshots__/MetricGroup.test.tsx.snap | 5070 ++++++------
.../__snapshots__/MetricPill.test.tsx.snap | 280 +-
.../AccountNotifications.test.tsx | 135 +-
.../notifications/NotificationFooter.test.tsx | 63 +-
.../notifications/NotificationHeader.test.tsx | 84 +-
.../notifications/NotificationRow.test.tsx | 140 +-
.../RepositoryNotifications.test.tsx | 63 +-
.../AccountNotifications.test.tsx.snap | 7211 +++++++++--------
.../NotificationFooter.test.tsx.snap | 2194 ++---
.../NotificationHeader.test.tsx.snap | 500 +-
.../NotificationRow.test.tsx.snap | 4540 ++++++-----
.../RepositoryNotifications.test.tsx.snap | 3157 ++++----
.../primitives/CustomCounter.test.tsx | 5 +-
.../components/primitives/EmojiText.test.tsx | 7 +-
.../components/primitives/Footer.test.tsx | 9 +-
.../components/primitives/Header.test.tsx | 28 +-
.../primitives/HoverButton.test.tsx | 7 +-
.../components/primitives/HoverGroup.test.tsx | 5 +-
.../components/primitives/Title.test.tsx | 9 +-
.../__snapshots__/CustomCounter.test.tsx.snap | 38 +-
.../__snapshots__/EmojiText.test.tsx.snap | 50 +-
.../__snapshots__/Footer.test.tsx.snap | 108 +-
.../__snapshots__/Header.test.tsx.snap | 238 +-
.../__snapshots__/HoverButton.test.tsx.snap | 160 +-
.../__snapshots__/HoverGroup.test.tsx.snap | 54 +-
.../__snapshots__/Title.test.tsx.snap | 158 +-
.../settings/AppearanceSettings.test.tsx | 112 +-
.../settings/NotificationSettings.test.tsx | 252 +-
.../settings/SettingsFooter.test.tsx | 49 +-
.../settings/SettingsReset.test.tsx | 34 +-
.../settings/SystemSettings.test.tsx | 157 +-
.../components/settings/TraySettings.test.tsx | 52 +-
.../SettingsFooter.test.tsx.snap | 2 +-
src/renderer/context/App.test.tsx | 73 +-
src/renderer/context/App.tsx | 2 +-
src/renderer/routes/Accounts.test.tsx | 264 +-
src/renderer/routes/Filters.test.tsx | 59 +-
src/renderer/routes/Login.test.tsx | 31 +-
.../routes/LoginWithOAuthApp.test.tsx | 61 +-
.../LoginWithPersonalAccessToken.test.tsx | 70 +-
src/renderer/routes/Notifications.test.tsx | 121 +-
src/renderer/routes/Settings.test.tsx | 25 +-
.../__snapshots__/Accounts.test.tsx.snap | 3697 ---------
.../__snapshots__/Filters.test.tsx.snap | 6 +-
.../routes/__snapshots__/Login.test.tsx.snap | 814 +-
.../LoginWithOAuthApp.test.tsx.snap | 1520 ++--
...LoginWithPersonalAccessToken.test.tsx.snap | 1368 ++--
.../__snapshots__/Notifications.test.tsx.snap | 272 +-
.../__snapshots__/Settings.test.tsx.snap | 40 +-
93 files changed, 23668 insertions(+), 25993 deletions(-)
create mode 100644 src/renderer/__helpers__/test-utils.tsx
delete mode 100644 src/renderer/routes/__snapshots__/Accounts.test.tsx.snap
diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx
new file mode 100644
index 000000000..1fdc1cbca
--- /dev/null
+++ b/src/renderer/__helpers__/test-utils.tsx
@@ -0,0 +1,140 @@
+import type { RenderOptions } from '@testing-library/react';
+import { render } from '@testing-library/react';
+import type { ReactElement, ReactNode } from 'react';
+import { useMemo } from 'react';
+
+import { BaseStyles, ThemeProvider } from '@primer/react';
+
+import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
+import type { AppContextState } from '../context/App';
+import { AppContext } from '../context/App';
+
+/**
+ * Props for the AppContextProvider wrapper
+ */
+interface AppContextProviderProps {
+ readonly children: ReactNode;
+ readonly value?: Partial;
+}
+
+/**
+ * Wrapper component that provides ThemeProvider, BaseStyles, and AppContext
+ * with sensible defaults for testing.
+ */
+export function AppContextProvider({
+ children,
+ value = {},
+}: AppContextProviderProps) {
+ const defaultValue: Partial = useMemo(() => {
+ return {
+ auth: mockAuth,
+ isLoggedIn: false,
+ loginWithGitHubApp: async () => {},
+ loginWithOAuthApp: async () => {},
+ loginWithPersonalAccessToken: async () => {},
+ logoutFromAccount: async () => {},
+
+ status: 'success',
+ globalError: { title: '', descriptions: [], emojis: [] },
+
+ notifications: [],
+ notificationCount: 0,
+ unreadNotificationCount: 0,
+ hasNotifications: false,
+ hasUnreadNotifications: false,
+
+ fetchNotifications: async () => {},
+ removeAccountNotifications: async () => {},
+
+ markNotificationsAsRead: async () => {},
+ markNotificationsAsDone: async () => {},
+ unsubscribeNotification: async () => {},
+
+ settings: mockSettings,
+ clearFilters: () => {},
+ resetSettings: () => {},
+ updateSetting: () => {},
+ updateFilter: () => {},
+
+ ...value,
+ } as Partial;
+ }, [value]);
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+/**
+ * Custom render function that wraps components with AppContextProvider by default.
+ *
+ * Usage (simplified):
+ * renderWithAppContext( , { auth, settings })
+ *
+ * Legacy (still supported):
+ * renderWithAppContext( , { appContext: { auth, settings } })
+ */
+type RenderWithAppContextOptions = Omit &
+ Partial & {
+ appContext?: Partial;
+ };
+
+export function renderWithAppContext(
+ ui: ReactElement,
+ options: RenderWithAppContextOptions = {},
+) {
+ const CONTEXT_KEYS: Array = [
+ 'auth',
+ 'isLoggedIn',
+ 'loginWithGitHubApp',
+ 'loginWithOAuthApp',
+ 'loginWithPersonalAccessToken',
+ 'logoutFromAccount',
+ 'status',
+ 'globalError',
+ 'notifications',
+ 'notificationCount',
+ 'unreadNotificationCount',
+ 'hasNotifications',
+ 'hasUnreadNotifications',
+ 'fetchNotifications',
+ 'removeAccountNotifications',
+ 'markNotificationsAsRead',
+ 'markNotificationsAsDone',
+ 'unsubscribeNotification',
+ 'settings',
+ 'clearFilters',
+ 'resetSettings',
+ 'updateSetting',
+ 'updateFilter',
+ ];
+
+ const { appContext, ...rest } = options as Partial> & {
+ appContext?: Partial;
+ };
+
+ const ctxFromTopLevel: Partial = {};
+ for (const key of CONTEXT_KEYS) {
+ if (key in rest && rest[key] !== undefined) {
+ (ctxFromTopLevel as Partial>)[key] = rest[key];
+ }
+ }
+
+ const value: Partial = { ...ctxFromTopLevel };
+ if (appContext) {
+ Object.assign(value, appContext);
+ }
+
+ return render(ui, {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ // No additional render options by default
+ });
+}
diff --git a/src/renderer/components/AllRead.test.tsx b/src/renderer/components/AllRead.test.tsx
index 743e4d6cf..30380ac74 100644
--- a/src/renderer/components/AllRead.test.tsx
+++ b/src/renderer/components/AllRead.test.tsx
@@ -1,8 +1,8 @@
-import { act, render } from '@testing-library/react';
+import { act } from '@testing-library/react';
+import { renderWithAppContext } from '../__helpers__/test-utils';
import { mockSettings } from '../__mocks__/state-mocks';
import { ensureStableEmojis } from '../__mocks__/utils';
-import { AppContext } from '../context/App';
import { AllRead } from './AllRead';
describe('renderer/components/AllRead.tsx', () => {
@@ -11,41 +11,27 @@ describe('renderer/components/AllRead.tsx', () => {
});
it('should render itself & its children - no filters', async () => {
- let tree: ReturnType | null = null;
+ let tree: ReturnType | null = null;
await act(async () => {
- tree = render(
-
-
- ,
- );
+ tree = renderWithAppContext( , {
+
+ settings: {
+ ...mockSettings } });
});
expect(tree).toMatchSnapshot();
});
it('should render itself & its children - with filters', async () => {
- let tree: ReturnType | null = null;
+ let tree: ReturnType | null = null;
await act(async () => {
- tree = render(
-
-
- ,
- );
+ tree = renderWithAppContext( , {
+
+ settings: {
+ ...mockSettings,
+ filterReasons: ['author'] } });
});
expect(tree).toMatchSnapshot();
diff --git a/src/renderer/components/Oops.test.tsx b/src/renderer/components/Oops.test.tsx
index 9eb16be2f..56ea2c9cb 100644
--- a/src/renderer/components/Oops.test.tsx
+++ b/src/renderer/components/Oops.test.tsx
@@ -1,5 +1,6 @@
-import { act, render } from '@testing-library/react';
+import { act } from '@testing-library/react';
+import { renderWithAppContext } from '../__helpers__/test-utils';
import { ensureStableEmojis } from '../__mocks__/utils';
import { Oops } from './Oops';
@@ -15,20 +16,20 @@ describe('renderer/components/Oops.tsx', () => {
emojis: ['🔥'],
};
- let tree: ReturnType | null = null;
+ let tree: ReturnType | null = null;
await act(async () => {
- tree = render( );
+ tree = renderWithAppContext( );
});
expect(tree).toMatchSnapshot();
});
it('should render itself & its children - fallback to unknown error', async () => {
- let tree: ReturnType | null = null;
+ let tree: ReturnType | null = null;
await act(async () => {
- tree = render( );
+ tree = renderWithAppContext( );
});
expect(tree).toMatchSnapshot();
diff --git a/src/renderer/components/Sidebar.test.tsx b/src/renderer/components/Sidebar.test.tsx
index 55c7186bf..38803f484 100644
--- a/src/renderer/components/Sidebar.test.tsx
+++ b/src/renderer/components/Sidebar.test.tsx
@@ -1,18 +1,17 @@
-import { render, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
+import { renderWithAppContext } from '../__helpers__/test-utils';
import { mockAccountNotifications } from '../__mocks__/notifications-mocks';
import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
-import { AppContext } from '../context/App';
import * as comms from '../utils/comms';
import { Sidebar } from './Sidebar';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
- useNavigate: () => mockNavigate,
-}));
+ useNavigate: () => mockNavigate }));
describe('renderer/components/Sidebar.tsx', () => {
const fetchNotifications = jest.fn();
@@ -25,56 +24,47 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('should render itself & its children (logged in)', () => {
- const tree = render(
-
+
+ ,
+ {
+
notifications: mockAccountNotifications,
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
expect(tree).toMatchSnapshot();
});
it('should render itself & its children (logged out)', () => {
- const tree = render(
-
+
+ ,
+ {
+
isLoggedIn: false,
notifications: mockAccountNotifications,
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
expect(tree).toMatchSnapshot();
});
it('should navigate home when clicking the gitify logo', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: false,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-home'));
@@ -84,19 +74,16 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('notifications icon', () => {
it('opens notifications home when clicked', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-notifications'));
@@ -108,38 +95,32 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('renders correct icon when there are no notifications', () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
expect(screen.getByTestId('sidebar-notifications')).toMatchSnapshot();
});
it('renders correct icon when there are notifications', () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
expect(screen.getByTestId('sidebar-notifications')).toMatchSnapshot();
@@ -148,19 +129,16 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('Filter notifications', () => {
it('go to the filters route', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-filter-notifications'));
@@ -169,19 +147,16 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('go to the home if filters path already shown', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-filter-notifications'));
@@ -192,19 +167,16 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('quick links', () => {
it('opens my github issues page', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: mockAccountNotifications,
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-my-issues'));
@@ -216,19 +188,16 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('opens my github pull requests page', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: mockAccountNotifications,
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-my-pull-requests'));
@@ -242,21 +211,18 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('Refresh Notifications', () => {
it('should refresh the notifications when status is not loading', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
settings: mockSettings,
fetchNotifications,
- status: 'success',
- }}
- >
-
-
-
- ,
+ status: 'success' },
);
await userEvent.click(screen.getByTestId('sidebar-refresh'));
@@ -265,21 +231,18 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('should not refresh the notifications when status is loading', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
settings: mockSettings,
fetchNotifications,
- status: 'loading',
- }}
- >
-
-
-
- ,
+ status: 'loading' },
);
await userEvent.click(screen.getByTestId('sidebar-refresh'));
@@ -290,19 +253,16 @@ describe('renderer/components/Sidebar.tsx', () => {
describe('Settings', () => {
it('go to the settings route', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-settings'));
@@ -311,20 +271,17 @@ describe('renderer/components/Sidebar.tsx', () => {
});
it('go to the home if settings path already shown', async () => {
- render(
-
+
+ ,
+ {
+
isLoggedIn: true,
notifications: [],
auth: mockAuth,
settings: mockSettings,
- fetchNotifications,
- }}
- >
-
-
-
- ,
+ fetchNotifications },
);
await userEvent.click(screen.getByTestId('sidebar-settings'));
@@ -337,19 +294,16 @@ describe('renderer/components/Sidebar.tsx', () => {
it('should quit the app', async () => {
const quitAppMock = jest.spyOn(comms, 'quitApp');
- render(
-
+
+ ,
+ {
+
isLoggedIn: false,
notifications: [],
auth: mockAuth,
- settings: mockSettings,
- }}
- >
-
-
-
- ,
+ settings: mockSettings },
);
await userEvent.click(screen.getByTestId('sidebar-quit'));
diff --git a/src/renderer/components/__snapshots__/AllRead.test.tsx.snap b/src/renderer/components/__snapshots__/AllRead.test.tsx.snap
index 2680d7bb3..7fb910ac5 100644
--- a/src/renderer/components/__snapshots__/AllRead.test.tsx.snap
+++ b/src/renderer/components/__snapshots__/AllRead.test.tsx.snap
@@ -6,45 +6,56 @@ exports[`renderer/components/AllRead.tsx should render itself & its children - n
"baseElement":
-
-
-
- No new notifications
+
+
+
+
+
+ No new notifications
+
+
@@ -53,45 +64,56 @@ exports[`renderer/components/AllRead.tsx should render itself & its children - n
,
"container":
-
-
-
- No new notifications
+
+
+
+
+
+ No new notifications
+
+
@@ -157,45 +179,56 @@ exports[`renderer/components/AllRead.tsx should render itself & its children - w
"baseElement":
-
-
-
- No new filtered notifications
+
+
+
+
+
+ No new filtered notifications
+
+
@@ -204,45 +237,56 @@ exports[`renderer/components/AllRead.tsx should render itself & its children - w
,
"container":
-
-
-
- No new filtered notifications
+
+
+
+
+
+ No new filtered notifications
+
+
diff --git a/src/renderer/components/__snapshots__/Oops.test.tsx.snap b/src/renderer/components/__snapshots__/Oops.test.tsx.snap
index 73af1dc40..e4a068b56 100644
--- a/src/renderer/components/__snapshots__/Oops.test.tsx.snap
+++ b/src/renderer/components/__snapshots__/Oops.test.tsx.snap
@@ -6,51 +6,62 @@ exports[`renderer/components/Oops.tsx should render itself & its children - fall
"baseElement":
-
+
+
+
+
+
+ Oops! Something went wrong
+
+
+
+ Please try again later.
+
-
- Oops! Something went wrong
-
-
-
- Please try again later.
@@ -58,52 +69,63 @@ exports[`renderer/components/Oops.tsx should render itself & its children - fall
,
"container":
-
-
-
- Oops! Something went wrong
+
+
+
+
+
+ Oops! Something went wrong
+
+
+
+ Please try again later.
+
-
- Please try again later.
-
,
@@ -167,51 +189,62 @@ exports[`renderer/components/Oops.tsx should render itself & its children - spec
"baseElement":
-
+
+
+
+
+
+ Error title
+
+
+
+ Error description
+
-
- Error title
-
-
-
- Error description
@@ -219,52 +252,63 @@ exports[`renderer/components/Oops.tsx should render itself & its children - spec
,
"container":
-
-
-
- Error title
+
+
+
+
+
+ Error title
+
+
+
+ Error description
+
-
- Error description
-
,
diff --git a/src/renderer/components/__snapshots__/Sidebar.test.tsx.snap b/src/renderer/components/__snapshots__/Sidebar.test.tsx.snap
index 69431d354..4d0498b4a 100644
--- a/src/renderer/components/__snapshots__/Sidebar.test.tsx.snap
+++ b/src/renderer/components/__snapshots__/Sidebar.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`renderer/components/Sidebar.tsx notifications icon renders correct icon when there are no notifications 1`] = `
-
-
-
- @gitify-app
+
+
+
+
+
+ @gitify-app
+
+
-
-
-
- @gitify-app
+
+
+
+
+
+ @gitify-app
+
+
-
- @gitify-app
+
+
+
+ @gitify-app
+
+
-
- @gitify-app
+
+
+
+ @gitify-app
+
+