diff --git a/src/components/Oops.test.tsx b/src/components/Oops.test.tsx
index 601f9ef0f..3d500f024 100644
--- a/src/components/Oops.test.tsx
+++ b/src/components/Oops.test.tsx
@@ -6,7 +6,7 @@ describe('components/Oops.tsx', () => {
it('should render itself & its children', () => {
const mockError = {
title: 'Error title',
- description: 'Error description',
+ descriptions: ['Error description'],
emojis: ['🔥'],
};
const tree = TestRenderer.create();
diff --git a/src/components/Oops.tsx b/src/components/Oops.tsx
index 2b6676464..e7a0201ab 100644
--- a/src/components/Oops.tsx
+++ b/src/components/Oops.tsx
@@ -19,7 +19,14 @@ export const Oops: FC = ({ error }) => {
{error.title}
- {error.description}
+ {error.descriptions.map((description, i) => {
+ return (
+ // biome-ignore lint/suspicious/noArrayIndexKey: using index for key to keep the error constants clean
+
+ {description}
+
+ );
+ })}
);
};
diff --git a/src/components/__snapshots__/Oops.test.tsx.snap b/src/components/__snapshots__/Oops.test.tsx.snap
index 4d777f8f1..614aca51f 100644
--- a/src/components/__snapshots__/Oops.test.tsx.snap
+++ b/src/components/__snapshots__/Oops.test.tsx.snap
@@ -14,7 +14,9 @@ exports[`components/Oops.tsx should render itself & its children 1`] = `
>
Error title
-
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 2a41d9185..f4cd786ed 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -1,5 +1,5 @@
import { act, renderHook, waitFor } from '@testing-library/react';
-import axios from 'axios';
+import axios, { AxiosError } from 'axios';
import nock from 'nock';
import { mockAccounts, mockSettings } from '../__mocks__/mock-state';
@@ -50,17 +50,20 @@ describe('hooks/useNotifications.ts', () => {
});
describe('should fetch notifications with failures - github.com & enterprise', () => {
- it('bad credentials', async () => {
- const status = 401;
- const message = 'Bad credentials';
+ it('network error', async () => {
+ const code = AxiosError.ERR_NETWORK;
nock('https://api.github.com/')
.get('/notifications?participating=false')
- .reply(status, { message });
+ .replyWithError({
+ code: code,
+ });
nock('https://github.gitify.io/api/v3/')
.get('/notifications?participating=false')
- .reply(status, { message });
+ .replyWithError({
+ code: code,
+ });
const { result } = renderHook(() => useNotifications());
@@ -70,93 +73,234 @@ describe('hooks/useNotifications.ts', () => {
await waitFor(() => {
expect(result.current.requestFailed).toBe(true);
- expect(result.current.errorDetails).toBe(Errors.BAD_CREDENTIALS);
+ expect(result.current.errorDetails).toBe(Errors.NETWORK);
});
});
- it('missing scopes', async () => {
- const status = 403;
- const message = "Missing the 'notifications' scope";
-
- nock('https://api.github.com/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ describe('bad request errors', () => {
+ it('bad credentials', async () => {
+ const code = AxiosError.ERR_BAD_REQUEST;
+ const status = 401;
+ const message = 'Bad credentials';
+
+ nock('https://api.github.com/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ nock('https://github.gitify.io/api/v3/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
- nock('https://github.gitify.io/api/v3/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ const { result } = renderHook(() => useNotifications());
- const { result } = renderHook(() => useNotifications());
+ act(() => {
+ result.current.fetchNotifications(mockAccounts, mockSettings);
+ });
- act(() => {
- result.current.fetchNotifications(mockAccounts, mockSettings);
+ await waitFor(() => {
+ expect(result.current.requestFailed).toBe(true);
+ expect(result.current.errorDetails).toBe(Errors.BAD_CREDENTIALS);
+ });
});
- await waitFor(() => {
- expect(result.current.requestFailed).toBe(true);
- expect(result.current.errorDetails).toBe(Errors.MISSING_SCOPES);
- });
- });
+ it('missing scopes', async () => {
+ const code = AxiosError.ERR_BAD_REQUEST;
+ const status = 403;
+ const message = "Missing the 'notifications' scope";
+
+ nock('https://api.github.com/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ nock('https://github.gitify.io/api/v3/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
- it('rate limited - primary', async () => {
- const status = 403;
- const message = 'API rate limit exceeded';
+ const { result } = renderHook(() => useNotifications());
- nock('https://api.github.com/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ act(() => {
+ result.current.fetchNotifications(mockAccounts, mockSettings);
+ });
- nock('https://github.gitify.io/api/v3/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ await waitFor(() => {
+ expect(result.current.requestFailed).toBe(true);
+ expect(result.current.errorDetails).toBe(Errors.MISSING_SCOPES);
+ });
+ });
- const { result } = renderHook(() => useNotifications());
+ it('rate limited - primary', async () => {
+ const code = AxiosError.ERR_BAD_REQUEST;
+ const status = 403;
+ const message = 'API rate limit exceeded';
+
+ nock('https://api.github.com/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ nock('https://github.gitify.io/api/v3/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
- act(() => {
- result.current.fetchNotifications(mockAccounts, mockSettings);
+ const { result } = renderHook(() => useNotifications());
+
+ act(() => {
+ result.current.fetchNotifications(mockAccounts, mockSettings);
+ });
+
+ await waitFor(() => {
+ expect(result.current.requestFailed).toBe(true);
+ expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
+ });
});
- await waitFor(() => {
- expect(result.current.requestFailed).toBe(true);
- expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
+ it('rate limited - secondary', async () => {
+ const code = AxiosError.ERR_BAD_REQUEST;
+ const status = 403;
+ const message = 'You have exceeded a secondary rate limit';
+
+ nock('https://api.github.com/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ nock('https://github.gitify.io/api/v3/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ const { result } = renderHook(() => useNotifications());
+
+ act(() => {
+ result.current.fetchNotifications(mockAccounts, mockSettings);
+ });
+
+ await waitFor(() => {
+ expect(result.current.requestFailed).toBe(true);
+ expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
+ });
});
- });
- it('rate limited - secondary', async () => {
- const status = 403;
- const message = 'You have exceeded a secondary rate limit';
+ it('unhandled bad request error', async () => {
+ const code = AxiosError.ERR_BAD_REQUEST;
+ const status = 400;
+ const message = 'Oops! Something went wrong.';
+
+ nock('https://api.github.com/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
+
+ nock('https://github.gitify.io/api/v3/')
+ .get('/notifications?participating=false')
+ .replyWithError({
+ code,
+ response: {
+ status,
+ data: {
+ message,
+ },
+ },
+ });
- nock('https://api.github.com/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ const { result } = renderHook(() => useNotifications());
- nock('https://github.gitify.io/api/v3/')
- .get('/notifications?participating=false')
- .reply(status, { message });
+ act(() => {
+ result.current.fetchNotifications(mockAccounts, mockSettings);
+ });
- const { result } = renderHook(() => useNotifications());
+ expect(result.current.isFetching).toBe(true);
- act(() => {
- result.current.fetchNotifications(mockAccounts, mockSettings);
- });
+ await waitFor(() => {
+ expect(result.current.isFetching).toBe(false);
+ });
- await waitFor(() => {
expect(result.current.requestFailed).toBe(true);
- expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
});
});
- it('default error', async () => {
- const status = 400;
- const message = 'Oops! Something went wrong.';
+ it('unknown error', async () => {
+ const code = 'anything';
nock('https://api.github.com/')
.get('/notifications?participating=false')
- .reply(status, { message });
+ .replyWithError({
+ code: code,
+ });
nock('https://github.gitify.io/api/v3/')
.get('/notifications?participating=false')
- .reply(status, { message });
+ .replyWithError({
+ code: code,
+ });
const { result } = renderHook(() => useNotifications());
@@ -164,13 +308,10 @@ describe('hooks/useNotifications.ts', () => {
result.current.fetchNotifications(mockAccounts, mockSettings);
});
- expect(result.current.isFetching).toBe(true);
-
await waitFor(() => {
- expect(result.current.isFetching).toBe(false);
+ expect(result.current.requestFailed).toBe(true);
+ expect(result.current.errorDetails).toBe(Errors.UNKNOWN);
});
-
- expect(result.current.requestFailed).toBe(true);
});
});
});
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 6fd6f7089..6489147fe 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -1,4 +1,4 @@
-import axios, { type AxiosError, type AxiosPromise } from 'axios';
+import axios, { AxiosError, type AxiosPromise } from 'axios';
import { useCallback, useState } from 'react';
import type {
@@ -405,6 +405,16 @@ export const useNotifications = (): NotificationsState => {
};
function determineFailureType(err: AxiosError): GitifyError {
+ const code = err.code;
+
+ if (code === AxiosError.ERR_NETWORK) {
+ return Errors.NETWORK;
+ }
+
+ if (code !== AxiosError.ERR_BAD_REQUEST) {
+ return Errors.UNKNOWN;
+ }
+
const status = err.response.status;
const message = err.response.data.message;
diff --git a/src/routes/__snapshots__/Notifications.test.tsx.snap b/src/routes/__snapshots__/Notifications.test.tsx.snap
index 6dd25e226..343e755d2 100644
--- a/src/routes/__snapshots__/Notifications.test.tsx.snap
+++ b/src/routes/__snapshots__/Notifications.test.tsx.snap
@@ -6,7 +6,9 @@ exports[`routes/Notifications.tsx should render itself & its children (error con
= {
BAD_CREDENTIALS: {
title: 'Bad Credentials',
- description: 'Your credentials are either invalid or expired.',
+ descriptions: ['Your credentials are either invalid or expired.'],
emojis: ['🔓'],
},
MISSING_SCOPES: {
title: 'Missing Scopes',
- description: 'Your credentials are missing a required API scope.',
+ descriptions: ['Your credentials are missing a required API scope.'],
emojis: ['🙃'],
},
+ NETWORK: {
+ title: 'Network Error',
+ descriptions: [
+ 'Unable to connect to one or more of your GitHub environments.',
+ 'Please check your network connection, including whether you require a VPN, and try again.',
+ ],
+ emojis: ['🛜'],
+ },
RATE_LIMITED: {
title: 'Rate Limited',
- description: 'Please wait a while before trying again.',
+ descriptions: ['Please wait a while before trying again.'],
emojis: ['😮💨'],
},
UNKNOWN: {
title: 'Oops! Something went wrong',
- description: 'Please try again later.',
+ descriptions: ['Please try again later.'],
emojis: ['🤔', '😞', '😤', '😱', '😭'],
},
};