diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 2e130bf6a..d652819ae 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,7 +8,7 @@ import { import { BaseStyles, ThemeProvider } from '@primer/react'; -import { AppProvider, useAppContext } from './context/App'; +import { AppProvider } from './context/App'; import { AccountsRoute } from './routes/Accounts'; import { FiltersRoute } from './routes/Filters'; import { LoginRoute } from './routes/Login'; @@ -17,14 +17,18 @@ import { LoginWithPersonalAccessTokenRoute } from './routes/LoginWithPersonalAcc import { NotificationsRoute } from './routes/Notifications'; import { SettingsRoute } from './routes/Settings'; +import { GlobalShortcuts } from './components/GlobalShortcuts'; import { AppLayout } from './components/layout/AppLayout'; import './App.css'; +import { useAppContext } from './hooks/useAppContext'; + function RequireAuth({ children }) { - const { isLoggedIn } = useAppContext(); const location = useLocation(); + const { isLoggedIn } = useAppContext(); + return isLoggedIn ? ( children ) : ( @@ -38,6 +42,7 @@ export const App = () => { + ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => navigateMock, +})); + +describe('components/GlobalShortcuts.tsx', () => { + const fetchNotificationsMock = jest.fn(); + const updateSettingMock = jest.fn(); + const quitAppSpy = jest.spyOn(comms, 'quitApp').mockImplementation(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('key bindings', () => { + describe('ignores keys that are not valid', () => { + it('ignores B key', async () => { + renderWithAppContext( + + + , + ); + + await userEvent.keyboard('b'); + + expect(navigateMock).not.toHaveBeenCalled(); + }); + }); + + describe('home', () => { + it('navigates home when pressing H key', async () => { + renderWithAppContext( + + + , + ); + + await userEvent.keyboard('h'); + + expect(navigateMock).toHaveBeenCalledWith('/', { replace: true }); + }); + }); + + describe('openGitHubNotifications', () => { + const openGitHubNotificationsSpy = jest + .spyOn(links, 'openGitHubNotifications') + .mockImplementation(); + + it('opens primary account GitHub notifications webpage when pressing N while logged in', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('n'); + + expect(openGitHubNotificationsSpy).toHaveBeenCalledTimes(1); + }); + + it('does not open primary account GitHub notifications webpage when logged out', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('n'); + + expect(openGitHubNotificationsSpy).not.toHaveBeenCalled(); + }); + }); + + describe('focus mode', () => { + it('toggles focus when pressing W while logged in', async () => { + renderWithAppContext( + + + , + { + updateSetting: updateSettingMock, + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('w'); + + expect(updateSettingMock).toHaveBeenCalledWith('participating', true); + }); + + it('does not toggle focus mode when loading', async () => { + renderWithAppContext( + + + , + { + updateSetting: updateSettingMock, + status: 'loading', + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('w'); + + expect(updateSettingMock).not.toHaveBeenCalled(); + }); + + it('does not toggle focus mode when logged out', async () => { + renderWithAppContext( + + + , + { + updateSetting: updateSettingMock, + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('w'); + + expect(updateSettingMock).not.toHaveBeenCalled(); + }); + }); + + describe('filters', () => { + it('toggles filters when pressing F while logged in', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('f'); + + expect(navigateMock).toHaveBeenCalledWith('/filters'); + }); + + it('does not toggle filters when logged out', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('f'); + + expect(navigateMock).not.toHaveBeenCalled(); + }); + }); + + describe('openGitHubIssues', () => { + const openGitHubIssuesSpy = jest + .spyOn(links, 'openGitHubIssues') + .mockImplementation(); + + it('opens primary account GitHub issues webpage when pressing I while logged in', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('i'); + + expect(openGitHubIssuesSpy).toHaveBeenCalledTimes(1); + }); + + it('does not open primary account GitHub issues webpage when logged out', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('n'); + + expect(openGitHubIssuesSpy).not.toHaveBeenCalled(); + }); + }); + + describe('openGitHubPulls', () => { + const openGitHubPullsSpy = jest + .spyOn(links, 'openGitHubPulls') + .mockImplementation(); + + it('opens primary account GitHub pull requests webpage when pressing N while logged in', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('p'); + + expect(openGitHubPullsSpy).toHaveBeenCalledTimes(1); + }); + + it('does not open primary account GitHub pull requests webpage when logged out', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('n'); + + expect(openGitHubPullsSpy).not.toHaveBeenCalled(); + }); + }); + + describe('refresh', () => { + it('refreshes notifications when pressing R key', async () => { + renderWithAppContext( + + + , + { + fetchNotifications: fetchNotificationsMock, + }, + ); + + await userEvent.keyboard('r'); + + expect(navigateMock).toHaveBeenCalledWith('/', { replace: true }); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(1); + }); + + it('does not refresh when status is loading', async () => { + renderWithAppContext( + + + , + { + status: 'loading', + }, + ); + + await userEvent.keyboard('r'); + + expect(fetchNotificationsMock).not.toHaveBeenCalled(); + }); + }); + + describe('settings', () => { + it('toggles settings when pressing S while logged in', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('s'); + + expect(navigateMock).toHaveBeenCalledWith('/settings'); + }); + + it('does not toggle settings when logged out', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: false, + }, + ); + + await userEvent.keyboard('s'); + + expect(navigateMock).not.toHaveBeenCalled(); + }); + }); + + describe('accounts', () => { + it('navigates to accounts when pressing A on settings route', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('a'); + + expect(navigateMock).toHaveBeenCalledWith('/accounts'); + }); + + it('does not trigger accounts when not on settings route', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('a'); + + expect(navigateMock).not.toHaveBeenCalledWith('/accounts'); + }); + }); + + describe('quit app', () => { + it('quits the app when pressing Q on settings route', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('q'); + + expect(quitAppSpy).toHaveBeenCalledTimes(1); + }); + + it('does not quit the app when not on settings route', async () => { + renderWithAppContext( + + + , + { + isLoggedIn: true, + }, + ); + + await userEvent.keyboard('q'); + + expect(quitAppSpy).not.toHaveBeenCalled(); + }); + }); + + describe('modifiers', () => { + it('ignores shortcuts when typing in an input', async () => { + renderWithAppContext( + + + + , + { + isLoggedIn: true, + }, + ); + + const input = document.getElementById( + 'test-input', + ) as HTMLTextAreaElement; + input.focus(); + await userEvent.type(input, 'h'); + + expect(navigateMock).not.toHaveBeenCalled(); + }); + + it('ignores shortcuts when typing in a textarea', async () => { + renderWithAppContext( + + +