Skip to content

Commit 40d3104

Browse files
setchyafonsojramos
andauthored
feat: new account management view (#1172)
* feat: add accounts route and view * feat: accounts view * add unit tests * feat: accounts view * feat: accounts view * add unit tests * add unit tests * add unit tests * add unit tests * move accounts back to settings * remove master logout * remove master logout * chore: merge `main` --------- Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
1 parent a342c17 commit 40d3104

File tree

11 files changed

+1023
-262
lines changed

11 files changed

+1023
-262
lines changed

src/__mocks__/state-mocks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export const mockGitHubEnterpriseServerAccount: Account = {
5353
user: mockGitifyUser,
5454
};
5555

56+
export const mockGitHubAppAccount: Account = {
57+
platform: 'GitHub Cloud',
58+
method: 'GitHub App',
59+
token: '987654321',
60+
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
61+
user: mockGitifyUser,
62+
};
63+
5664
export const mockAuth: AuthState = {
5765
accounts: [mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount],
5866
};

src/app.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { Loading } from './components/Loading';
1010
import { Sidebar } from './components/Sidebar';
1111
import { AppContext, AppProvider } from './context/App';
12+
import { AccountsRoute } from './routes/Accounts';
1213
import { LoginRoute } from './routes/Login';
1314
import { LoginWithOAuthApp } from './routes/LoginWithOAuthApp';
1415
import { LoginWithPersonalAccessToken } from './routes/LoginWithPersonalAccessToken';
@@ -50,7 +51,14 @@ export const App = () => {
5051
</RequireAuth>
5152
}
5253
/>
53-
54+
<Route
55+
path="/accounts"
56+
element={
57+
<RequireAuth>
58+
<AccountsRoute />
59+
</RequireAuth>
60+
}
61+
/>
5462
<Route path="/login" element={<LoginRoute />} />
5563
<Route
5664
path="/login-personal-access-token"

src/context/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { useInterval } from '../hooks/useInterval';
1010
import { useNotifications } from '../hooks/useNotifications';
1111
import {
12+
type Account,
1213
type AccountNotifications,
1314
type AuthState,
1415
type GitifyError,
@@ -27,6 +28,7 @@ import {
2728
authGitHub,
2829
getToken,
2930
getUserData,
31+
removeAccount,
3032
} from '../utils/auth/utils';
3133
import { setAutoLaunch, updateTrayTitle } from '../utils/comms';
3234
import Constants from '../utils/constants';
@@ -62,6 +64,7 @@ interface AppContextState {
6264
loginWithGitHubApp: () => void;
6365
loginWithOAuthApp: (data: LoginOAuthAppOptions) => void;
6466
loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => void;
67+
logoutFromAccount: (account: Account) => void;
6568
logout: () => void;
6669

6770
notifications: AccountNotifications[];
@@ -193,6 +196,15 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
193196
[auth, settings],
194197
);
195198

199+
const logoutFromAccount = useCallback(
200+
async (account: Account) => {
201+
const updatedAuth = removeAccount(auth, account);
202+
setAuth(updatedAuth);
203+
saveState({ auth: updatedAuth, settings });
204+
},
205+
[auth, settings],
206+
);
207+
196208
const logout = useCallback(() => {
197209
setAuth(defaultAuth);
198210
clearState();
@@ -256,6 +268,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
256268
loginWithGitHubApp,
257269
loginWithOAuthApp,
258270
loginWithPersonalAccessToken,
271+
logoutFromAccount,
259272
logout,
260273

261274
notifications,

src/routes/Accounts.test.tsx

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
2+
import { ipcRenderer, shell } from 'electron';
3+
import { MemoryRouter } from 'react-router-dom';
4+
import {
5+
mockAuth,
6+
mockGitHubAppAccount,
7+
mockOAuthAccount,
8+
mockPersonalAccessTokenAccount,
9+
mockSettings,
10+
} from '../__mocks__/state-mocks';
11+
import { AppContext } from '../context/App';
12+
import { AccountsRoute } from './Accounts';
13+
14+
const mockNavigate = jest.fn();
15+
jest.mock('react-router-dom', () => ({
16+
...jest.requireActual('react-router-dom'),
17+
useNavigate: () => mockNavigate,
18+
}));
19+
20+
describe('routes/Accounts.tsx', () => {
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
describe('General', () => {
26+
it('should render itself & its children', async () => {
27+
await act(async () => {
28+
render(
29+
<AppContext.Provider
30+
value={{
31+
auth: {
32+
accounts: [
33+
mockPersonalAccessTokenAccount,
34+
mockOAuthAccount,
35+
mockGitHubAppAccount,
36+
],
37+
},
38+
settings: mockSettings,
39+
}}
40+
>
41+
<MemoryRouter>
42+
<AccountsRoute />
43+
</MemoryRouter>
44+
</AppContext.Provider>,
45+
);
46+
});
47+
48+
expect(screen.getByTestId('accounts')).toMatchSnapshot();
49+
});
50+
51+
it('should go back by pressing the icon', async () => {
52+
await act(async () => {
53+
render(
54+
<AppContext.Provider
55+
value={{
56+
auth: mockAuth,
57+
settings: mockSettings,
58+
}}
59+
>
60+
<MemoryRouter>
61+
<AccountsRoute />
62+
</MemoryRouter>
63+
</AppContext.Provider>,
64+
);
65+
});
66+
67+
fireEvent.click(screen.getByLabelText('Go Back'));
68+
expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
69+
});
70+
});
71+
72+
describe('Account interactions', () => {
73+
it('open profile in external browser', async () => {
74+
await act(async () => {
75+
render(
76+
<AppContext.Provider
77+
value={{
78+
auth: {
79+
accounts: [mockPersonalAccessTokenAccount],
80+
},
81+
settings: mockSettings,
82+
}}
83+
>
84+
<MemoryRouter>
85+
<AccountsRoute />
86+
</MemoryRouter>
87+
</AppContext.Provider>,
88+
);
89+
});
90+
91+
fireEvent.click(screen.getByTitle('Open Profile'));
92+
93+
expect(shell.openExternal).toHaveBeenCalledTimes(1);
94+
expect(shell.openExternal).toHaveBeenCalledWith(
95+
'https://github.com/octocat',
96+
);
97+
});
98+
99+
it('open host in external browser', async () => {
100+
await act(async () => {
101+
render(
102+
<AppContext.Provider
103+
value={{
104+
auth: {
105+
accounts: [mockPersonalAccessTokenAccount],
106+
},
107+
settings: mockSettings,
108+
}}
109+
>
110+
<MemoryRouter>
111+
<AccountsRoute />
112+
</MemoryRouter>
113+
</AppContext.Provider>,
114+
);
115+
});
116+
117+
fireEvent.click(screen.getByTitle('Open Host'));
118+
119+
expect(shell.openExternal).toHaveBeenCalledTimes(1);
120+
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com');
121+
});
122+
123+
it('open developer settings in external browser', async () => {
124+
await act(async () => {
125+
render(
126+
<AppContext.Provider
127+
value={{
128+
auth: {
129+
accounts: [mockPersonalAccessTokenAccount],
130+
},
131+
settings: mockSettings,
132+
}}
133+
>
134+
<MemoryRouter>
135+
<AccountsRoute />
136+
</MemoryRouter>
137+
</AppContext.Provider>,
138+
);
139+
});
140+
141+
fireEvent.click(screen.getByTitle('Open Developer Settings'));
142+
143+
expect(shell.openExternal).toHaveBeenCalledTimes(1);
144+
expect(shell.openExternal).toHaveBeenCalledWith(
145+
'https://github.com/settings/tokens',
146+
);
147+
});
148+
149+
it('should logout', async () => {
150+
const logoutFromAccountMock = jest.fn();
151+
await act(async () => {
152+
render(
153+
<AppContext.Provider
154+
value={{
155+
auth: {
156+
accounts: [mockPersonalAccessTokenAccount],
157+
},
158+
settings: mockSettings,
159+
logoutFromAccount: logoutFromAccountMock,
160+
}}
161+
>
162+
<MemoryRouter>
163+
<AccountsRoute />
164+
</MemoryRouter>
165+
</AppContext.Provider>,
166+
);
167+
});
168+
169+
fireEvent.click(screen.getByTitle('Logout octocat'));
170+
171+
expect(logoutFromAccountMock).toHaveBeenCalledTimes(1);
172+
173+
expect(ipcRenderer.send).toHaveBeenCalledTimes(2);
174+
expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
175+
expect(ipcRenderer.send).toHaveBeenCalledWith('update-title', '');
176+
expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
177+
});
178+
});
179+
180+
describe('Add new accounts', () => {
181+
describe('Login with Personal Access Token', () => {
182+
it('should show login with personal access token button if not logged in', async () => {
183+
await act(async () => {
184+
render(
185+
<AppContext.Provider
186+
value={{
187+
auth: { accounts: [mockOAuthAccount] },
188+
settings: mockSettings,
189+
}}
190+
>
191+
<MemoryRouter>
192+
<AccountsRoute />
193+
</MemoryRouter>
194+
</AppContext.Provider>,
195+
);
196+
});
197+
198+
expect(
199+
screen.getByTitle('Login with Personal Access Token').hidden,
200+
).toBe(false);
201+
202+
fireEvent.click(screen.getByTitle('Login with Personal Access Token'));
203+
expect(mockNavigate).toHaveBeenNthCalledWith(
204+
1,
205+
'/login-personal-access-token',
206+
{
207+
replace: true,
208+
},
209+
);
210+
});
211+
212+
it('should hide login with personal access token button if already logged in', async () => {
213+
await act(async () => {
214+
render(
215+
<AppContext.Provider
216+
value={{
217+
auth: { accounts: [mockPersonalAccessTokenAccount] },
218+
settings: mockSettings,
219+
}}
220+
>
221+
<MemoryRouter>
222+
<AccountsRoute />
223+
</MemoryRouter>
224+
</AppContext.Provider>,
225+
);
226+
});
227+
228+
expect(
229+
screen.getByTitle('Login with Personal Access Token').hidden,
230+
).toBe(true);
231+
});
232+
});
233+
234+
describe('Login with OAuth App', () => {
235+
it('should show login with oauth app if not logged in', async () => {
236+
await act(async () => {
237+
render(
238+
<AppContext.Provider
239+
value={{
240+
auth: { accounts: [mockPersonalAccessTokenAccount] },
241+
settings: mockSettings,
242+
}}
243+
>
244+
<MemoryRouter>
245+
<AccountsRoute />
246+
</MemoryRouter>
247+
</AppContext.Provider>,
248+
);
249+
});
250+
251+
expect(screen.getByTitle('Login with OAuth App').hidden).toBe(false);
252+
253+
fireEvent.click(screen.getByTitle('Login with OAuth App'));
254+
expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-oauth-app', {
255+
replace: true,
256+
});
257+
});
258+
259+
it('should hide login with oauth app route if already logged in', async () => {
260+
await act(async () => {
261+
render(
262+
<AppContext.Provider
263+
value={{
264+
auth: { accounts: [mockOAuthAccount] },
265+
settings: mockSettings,
266+
}}
267+
>
268+
<MemoryRouter>
269+
<AccountsRoute />
270+
</MemoryRouter>
271+
</AppContext.Provider>,
272+
);
273+
});
274+
275+
expect(screen.getByTitle('Login with OAuth App').hidden).toBe(true);
276+
});
277+
});
278+
});
279+
});

0 commit comments

Comments
 (0)