diff --git a/src/renderer/__helpers__/test-utils.tsx b/src/renderer/__helpers__/test-utils.tsx index 6765591a5..e6cd951e7 100644 --- a/src/renderer/__helpers__/test-utils.tsx +++ b/src/renderer/__helpers__/test-utils.tsx @@ -1,5 +1,6 @@ import { render } from '@testing-library/react'; import { type ReactElement, type ReactNode, useMemo } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { BaseStyles, ThemeProvider } from '@primer/react'; @@ -7,22 +8,39 @@ import { mockAuth, mockSettings } from '../__mocks__/state-mocks'; import { AppContext, type AppContextState } from '../context/App'; +export { navigateMock } from './vitest.setup'; export type DeepPartial = { [K in keyof T]?: DeepPartial }; +const EMPTY_APP_CONTEXT: TestAppContext = {}; + +interface RenderOptions extends Partial { + initialEntries?: string[]; +} + +/** + * Test context + */ +type TestAppContext = Partial; + /** * Props for the AppContextProvider wrapper */ interface AppContextProviderProps { readonly children: ReactNode; - readonly value?: Partial; + readonly value?: TestAppContext; + readonly initialEntries?: string[]; } /** * Wrapper component that provides ThemeProvider, BaseStyles, and AppContext * with sensible defaults for testing. */ -function AppContextProvider({ children, value = {} }: AppContextProviderProps) { - const defaultValue: AppContextState = useMemo(() => { +function AppContextProvider({ + children, + value = EMPTY_APP_CONTEXT, + initialEntries, +}: AppContextProviderProps) { + const defaultValue: TestAppContext = useMemo(() => { return { auth: mockAuth, settings: mockSettings, @@ -58,17 +76,19 @@ function AppContextProvider({ children, value = {} }: AppContextProviderProps) { updateFilter: vi.fn(), ...value, - } as AppContextState; + } as TestAppContext; }, [value]); return ( - - - - {children} - - - + + + + + {children} + + + + ); } @@ -80,11 +100,13 @@ function AppContextProvider({ children, value = {} }: AppContextProviderProps) { */ export function renderWithAppContext( ui: ReactElement, - context: Partial = {}, + { initialEntries, ...context }: RenderOptions = {}, ) { return render(ui, { wrapper: ({ children }) => ( - {children} + + {children} + ), }); } diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts index 484651a00..30b4e80a5 100644 --- a/src/renderer/__helpers__/vitest.setup.ts +++ b/src/renderer/__helpers__/vitest.setup.ts @@ -2,6 +2,15 @@ import '@testing-library/jest-dom/vitest'; import { useFiltersStore } from '../stores'; +/** + * Shared navigate mock — import from test-utils in any test that needs to assert on navigation + */ +export const navigateMock = vi.fn(); +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useNavigate: () => navigateMock, +})); + // Sets timezone to UTC for consistent date/time in tests and snapshots process.env.TZ = 'UTC'; @@ -10,6 +19,7 @@ process.env.TZ = 'UTC'; */ beforeEach(() => { useFiltersStore.getState().reset(); + navigateMock.mockReset(); }); /** diff --git a/src/renderer/components/GlobalShortcuts.test.tsx b/src/renderer/components/GlobalShortcuts.test.tsx index b7ac6669b..e46b92a2c 100644 --- a/src/renderer/components/GlobalShortcuts.test.tsx +++ b/src/renderer/components/GlobalShortcuts.test.tsx @@ -1,18 +1,11 @@ import userEvent from '@testing-library/user-event'; -import { MemoryRouter } from 'react-router-dom'; -import { renderWithAppContext } from '../__helpers__/test-utils'; +import { navigateMock, renderWithAppContext } from '../__helpers__/test-utils'; import * as comms from '../utils/comms'; import * as links from '../utils/links'; import { GlobalShortcuts } from './GlobalShortcuts'; -const navigateMock = vi.fn(); -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useNavigate: () => navigateMock, -})); - describe('components/GlobalShortcuts.tsx', () => { const fetchNotificationsMock = vi.fn(); const updateSettingMock = vi.fn(); @@ -25,11 +18,7 @@ describe('components/GlobalShortcuts.tsx', () => { describe('key bindings', () => { describe('ignores keys that are not valid', () => { it('ignores B key', async () => { - renderWithAppContext( - - - , - ); + renderWithAppContext(); await userEvent.keyboard('b'); @@ -39,11 +28,7 @@ describe('components/GlobalShortcuts.tsx', () => { describe('home', () => { it('navigates home when pressing H key', async () => { - renderWithAppContext( - - - , - ); + renderWithAppContext(); await userEvent.keyboard('h'); @@ -57,14 +42,9 @@ describe('components/GlobalShortcuts.tsx', () => { .mockImplementation(vi.fn()); it('opens primary account GitHub notifications webpage when pressing N while logged in', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('n'); @@ -72,14 +52,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not open primary account GitHub notifications webpage when logged out', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + isLoggedIn: false, + }); await userEvent.keyboard('n'); @@ -89,15 +64,10 @@ describe('components/GlobalShortcuts.tsx', () => { describe('focus mode', () => { it('toggles focus when pressing W while logged in', async () => { - renderWithAppContext( - - - , - { - updateSetting: updateSettingMock, - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + updateSetting: updateSettingMock, + isLoggedIn: true, + }); await userEvent.keyboard('w'); @@ -105,16 +75,11 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not toggle focus mode when loading', async () => { - renderWithAppContext( - - - , - { - updateSetting: updateSettingMock, - status: 'loading', - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + updateSetting: updateSettingMock, + status: 'loading', + isLoggedIn: true, + }); await userEvent.keyboard('w'); @@ -122,15 +87,10 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not toggle focus mode when logged out', async () => { - renderWithAppContext( - - - , - { - updateSetting: updateSettingMock, - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + updateSetting: updateSettingMock, + isLoggedIn: false, + }); await userEvent.keyboard('w'); @@ -140,14 +100,9 @@ describe('components/GlobalShortcuts.tsx', () => { describe('filters', () => { it('toggles filters when pressing F while logged in', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('f'); @@ -155,14 +110,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not toggle filters when logged out', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + isLoggedIn: false, + }); await userEvent.keyboard('f'); @@ -176,14 +126,9 @@ describe('components/GlobalShortcuts.tsx', () => { .mockImplementation(vi.fn()); it('opens primary account GitHub issues webpage when pressing I while logged in', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('i'); @@ -191,14 +136,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not open primary account GitHub issues webpage when logged out', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + isLoggedIn: false, + }); await userEvent.keyboard('n'); @@ -212,14 +152,9 @@ describe('components/GlobalShortcuts.tsx', () => { .mockImplementation(vi.fn()); it('opens primary account GitHub pull requests webpage when pressing N while logged in', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('p'); @@ -227,14 +162,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not open primary account GitHub pull requests webpage when logged out', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + isLoggedIn: false, + }); await userEvent.keyboard('n'); @@ -244,14 +174,9 @@ describe('components/GlobalShortcuts.tsx', () => { describe('refresh', () => { it('refreshes notifications when pressing R key', async () => { - renderWithAppContext( - - - , - { - fetchNotifications: fetchNotificationsMock, - }, - ); + renderWithAppContext(, { + fetchNotifications: fetchNotificationsMock, + }); await userEvent.keyboard('r'); @@ -260,14 +185,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not refresh when status is loading', async () => { - renderWithAppContext( - - - , - { - status: 'loading', - }, - ); + renderWithAppContext(, { + status: 'loading', + }); await userEvent.keyboard('r'); @@ -277,14 +197,9 @@ describe('components/GlobalShortcuts.tsx', () => { describe('settings', () => { it('toggles settings when pressing S while logged in', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('s'); @@ -292,14 +207,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not toggle settings when logged out', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: false, - }, - ); + renderWithAppContext(, { + isLoggedIn: false, + }); await userEvent.keyboard('s'); @@ -309,14 +219,10 @@ describe('components/GlobalShortcuts.tsx', () => { describe('accounts', () => { it('navigates to accounts when pressing A on settings route', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + initialEntries: ['/settings'], + isLoggedIn: true, + }); await userEvent.keyboard('a'); @@ -324,14 +230,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not trigger accounts when not on settings route', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('a'); @@ -341,14 +242,10 @@ describe('components/GlobalShortcuts.tsx', () => { describe('quit app', () => { it('quits the app when pressing Q on settings route', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + initialEntries: ['/settings'], + isLoggedIn: true, + }); await userEvent.keyboard('q'); @@ -356,14 +253,9 @@ describe('components/GlobalShortcuts.tsx', () => { }); it('does not quit the app when not on settings route', async () => { - renderWithAppContext( - - - , - { - isLoggedIn: true, - }, - ); + renderWithAppContext(, { + isLoggedIn: true, + }); await userEvent.keyboard('q'); @@ -374,10 +266,10 @@ describe('components/GlobalShortcuts.tsx', () => { describe('modifiers', () => { it('ignores shortcuts when typing in an input', async () => { renderWithAppContext( - + <> - , + , { isLoggedIn: true, }, @@ -394,10 +286,10 @@ describe('components/GlobalShortcuts.tsx', () => { it('ignores shortcuts when typing in a textarea', async () => { renderWithAppContext( - + <>