Skip to content

Commit

Permalink
feat: Reset game when it is unknown, improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
barnslig committed Dec 30, 2021
1 parent 415d5a1 commit 8f6ea8a
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 43 deletions.
9 changes: 6 additions & 3 deletions app/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import theme from "./theme";
import AppRouter from "./AppRouter";
import NotificationProvider from "./NotificationProvider";
import PwaReloadNotification from "./PwaReloadNotification";
import SWRProvider from "./SWRProvider";

const App = () => {
return (
Expand All @@ -17,9 +18,11 @@ const App = () => {
{/* @ts-expect-error */}
<IntlProvider messages={messages} locale="de" defaultLocale="de">
<NotificationProvider>
<CssBaseline />
<PwaReloadNotification />
<AppRouter />
<SWRProvider>
<CssBaseline />
<PwaReloadNotification />
<AppRouter />
</SWRProvider>
</NotificationProvider>
</IntlProvider>
</ThemeProvider>
Expand Down
31 changes: 31 additions & 0 deletions app/src/app/SWRProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SWRConfig } from "swr";
import * as React from "react";

import ApiError from "../common/hooks/api/helper/ApiError";
import useHandleApiError from "../common/hooks/api/useHandleApiError";

type SWRProviderProps = {
children: React.ReactNode;
};

/**
* A provider that enables global error handling for SWR hooks
*/
const SWRProvider = ({ children }: SWRProviderProps) => {
const handleApiError = useHandleApiError();

const onError = React.useCallback(
(e: Error) => {
if (!(e instanceof ApiError)) {
return;
}

handleApiError(e);
},
[handleApiError]
);

return <SWRConfig value={{ onError }}>{children}</SWRConfig>;
};

export default SWRProvider;
6 changes: 1 addition & 5 deletions app/src/app/pages/CodePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,19 @@ import ButtonProgressIndicator from "../../common/components/ButtonProgressIndic
import Code from "../../features/code/Code";
import StickyActionButtons from "../../common/components/StickyActionButtons";
import useCode from "../../common/hooks/api/useCode";
import useHandleApiError from "../../common/hooks/api/useHandleApiError";
import useSubmitCode from "../../common/hooks/api/useSubmitCode";

type CodePageProps = {
codeId: string;
};

const CodePage = ({ codeId }: CodePageProps) => {
const { data, error } = useCode(codeId);
const { data } = useCode(codeId);
const intl = useIntl();
const [isLoading, submitCode] = useSubmitCode(codeId);

const code = data?.data;

const handleError = useHandleApiError();
React.useEffect(() => handleError(error), [handleError, error]);

const onSubmit = () => {
if (!code) {
return;
Expand Down
13 changes: 10 additions & 3 deletions app/src/common/hooks/api/helper/ApiError.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
interface ApiErrorResponseError {
id: string;
export type ApiErrorResponseTypes =
| "already-used"
| "code-not-found"
| "game-not-found"
| "not-authorised"
| "unknown-error";

export interface ApiErrorResponseError {
id: ApiErrorResponseTypes;
status: number;
title: string;
}

interface ApiErrorResponse {
export interface ApiErrorResponse {
errors: ApiErrorResponseError[];
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/common/hooks/api/helper/errorAwareFetcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ApiError from "./ApiError";
import ApiError, { ApiErrorResponse } from "./ApiError";
import errorAwareFetcher from "./errorAwareFetcher";

it("throws an error when the response is not ok", async () => {
const mockError = {
const mockError: ApiErrorResponse = {
errors: [
{
id: "unknown-error",
Expand Down
56 changes: 49 additions & 7 deletions app/src/common/hooks/api/useHandleApiError.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as React from "react";

import render from "../../testing/render";

import * as useCharacter from "../useCharacter";
import * as useGameId from "../useGameId";
import ApiError from "./helper/ApiError";
import useHandleApiError from "./useHandleApiError";

Expand All @@ -11,10 +13,15 @@ const error = new ApiError("test error");
const errorWithInfo = new ApiError("test error", 400, {
errors: [
{
id: "test-error",
id: "unknown-error",
status: 400,
title: "test error 123",
},
{
id: "unknown-error",
status: 500,
title: "other test error 1234",
},
],
});

Expand All @@ -38,15 +45,50 @@ const TestComponent = ({ error }: TestComponentProps) => {
it("enqueues an error snack with a title", async () => {
render(<TestComponent error={errorWithInfo} />);

await waitFor(() => screen.getByRole("alert"));
expect(screen.getByRole("alert")).toHaveTextContent("test error 123");
await waitFor(() => {
const errors = screen.getAllByRole("alert");
expect(errors[0]).toHaveTextContent("test error 123");
expect(errors[1]).toHaveTextContent("other test error 1234");
});
});

it("enqueues an error with a default error message when no info is available", async () => {
render(<TestComponent error={error} />);

await waitFor(() => screen.getByRole("alert"));
expect(screen.getByRole("alert")).toHaveTextContent(
"Ein Fehler ist aufgetreten!"
);
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(
"Ein Fehler ist aufgetreten!"
);
});
});

it("resets the game when a game-not-found error appears", async () => {
const deleteCharacter = jest.fn();
jest
.spyOn(useCharacter, "default")
.mockReturnValue(["", () => {}, deleteCharacter]);

const deleteGameId = jest.fn();
jest
.spyOn(useGameId, "default")
.mockReturnValue(["", () => {}, deleteGameId]);

const error = new ApiError("test error", 404, {
errors: [
{
id: "game-not-found",
status: 404,
title: "Unknown game",
},
],
});

render(<TestComponent error={error} />);

await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent("Unknown game");
});

expect(deleteCharacter).toHaveBeenCalled();
expect(deleteGameId).toHaveBeenCalled();
});
56 changes: 42 additions & 14 deletions app/src/common/hooks/api/useHandleApiError.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
import { useIntl } from "react-intl";
import { useLocation } from "wouter";
import { useSnackbar } from "notistack";
import * as React from "react";

import ApiError from "./helper/ApiError";
import useCharacter from "../useCharacter";
import useGameId from "../useGameId";

/**
* A React hook that provides error handling for the ApiError
*
* @returns A method that handles an API error
*/
const useHandleApiError = () => {
const [, , deleteCharacter] = useCharacter();
const [, , deleteGameId] = useGameId();
const [, setLocation] = useLocation();
const { enqueueSnackbar } = useSnackbar();
const intl = useIntl();

return (error?: ApiError) => {
if (!error) {
return;
}
return React.useCallback(
(error?: ApiError) => {
if (!error) {
return;
}

const title = error.info?.errors[0].title;

enqueueSnackbar(
title
? title
: intl.formatMessage({
if (!error.info || error.info.errors.length === 0) {
/* Show general error when no error info is set. This case only
* happens when the request completely failed, e.g. by a
* network error.
*/
enqueueSnackbar(
intl.formatMessage({
defaultMessage: "Ein Fehler ist aufgetreten!",
description: "unknown error snack",
}),
{ variant: "error" }
);
{ variant: "error" }
);

setLocation("/");

return;
}

const { errors } = error.info;
for (let i = 0; i < errors.length; i += 1) {
const err = errors[i];

/* Reset game when the game ID is unknown and redirect to the
* join game start page.
*/
if (err.id === "game-not-found") {
deleteCharacter();
deleteGameId();
}

setLocation("/");
};
enqueueSnackbar(err.title, { variant: "error" });
setLocation("/");
}
},
[deleteCharacter, deleteGameId, enqueueSnackbar, intl, setLocation]
);
};

export default useHandleApiError;
2 changes: 1 addition & 1 deletion app/src/mocks/data/code-not-found.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"errors": [
{
"id": "not-found",
"id": "code-not-found",
"status": 404,
"title": "Unbekannter QR-Code."
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/mocks/data/game-not-found.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"errors": [
{
"id": "not-found",
"id": "game-not-found",
"status": 404,
"title": "Unknown game"
}
Expand Down
2 changes: 1 addition & 1 deletion backend/dpt_app/trails/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

urlpatterns = [
path('', views.index, name='index'),
path('games/<str:gameId>/', views.gameManifest, name='gameManifest'),
path('games/<str:gameId>', views.gameManifest, name='gameManifest'),
path('games/<str:gameId>/clock', views.clock, name='clock'),
path('games/<str:gameId>/parameters', views.parameter, name='parameter'),
path('games/<str:gameId>/codes/<str:codeId>', views.code, name='code'),
Expand Down
4 changes: 2 additions & 2 deletions backend/dpt_app/trails/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def inner(request, gameId, *args, **kwargs):
except Game.DoesNotExist:
return JsonResponse({"errors": [
{
"id": "not-found",
"id": "game-not-found",
"status": 404,
"title": "Unknown Game ID"
}
Expand Down Expand Up @@ -149,7 +149,7 @@ def has_valid_bearer(bearer, game):
except Code.DoesNotExist:
return JsonResponse({"errors": [
{
"id": "not-found",
"id": "code-not-found",
"status": 404,
"title": "Unknown QR code ID"
}
Expand Down
8 changes: 4 additions & 4 deletions docs/ABC-DPT.v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ paths:
items:
$ref: "#/components/schemas/Error"
examples:
not-found:
code-not-found:
value:
errors:
- id: not-found
- id: code-not-found
status: 404
title: Unknown QR code ID
description: Get the actions represented by a QR code id.
Expand Down Expand Up @@ -400,10 +400,10 @@ paths:
items:
$ref: "#/components/schemas/Error"
examples:
not-found:
game-not-found:
value:
errors:
- id: not-found
- id: game-not-found
status: 404
title: Unknown game
operationId: get-game
Expand Down

0 comments on commit 8f6ea8a

Please sign in to comment.