diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8d8e988..adc101a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@ericblade/quagga2": "^1.8.4",
- "@tanstack/react-query": "^5.52.1",
+ "@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
@@ -22,6 +22,7 @@
"date-fns": "^3.6.0",
"fast-xml-parser": "^4.5.0",
"history": "^5.3.0",
+ "jest-canvas-mock": "^2.5.2",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
@@ -3727,9 +3728,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.56.2",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz",
- "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==",
+ "version": "5.59.13",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz",
+ "integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3737,12 +3738,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.56.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz",
- "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==",
+ "version": "5.59.15",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz",
+ "integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.56.2"
+ "@tanstack/query-core": "5.59.13"
},
"funding": {
"type": "github",
@@ -6655,6 +6656,12 @@
"node": ">=4"
}
},
+ "node_modules/cssfontparser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
+ "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
+ "license": "MIT"
+ },
"node_modules/cssnano": {
"version": "5.1.15",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
@@ -10610,6 +10617,16 @@
}
}
},
+ "node_modules/jest-canvas-mock": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
+ "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
+ "license": "MIT",
+ "dependencies": {
+ "cssfontparser": "^1.2.1",
+ "moo-color": "^1.0.2"
+ }
+ },
"node_modules/jest-changed-files": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
@@ -12147,6 +12164,15 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/moo-color": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
+ "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.1.4"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index c1ddcba..807b769 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,7 +5,7 @@
"proxy": "http://localhost:3001",
"dependencies": {
"@ericblade/quagga2": "^1.8.4",
- "@tanstack/react-query": "^5.52.1",
+ "@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
@@ -18,6 +18,7 @@
"date-fns": "^3.6.0",
"fast-xml-parser": "^4.5.0",
"history": "^5.3.0",
+ "jest-canvas-mock": "^2.5.2",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
deleted file mode 100644
index 2a68616..0000000
--- a/frontend/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/frontend/src/shared/utils/__tests__/getMedianOfCodeErrors.test.ts b/frontend/src/shared/utils/__tests__/getMedianOfCodeErrors.test.ts
new file mode 100644
index 0000000..8411e00
--- /dev/null
+++ b/frontend/src/shared/utils/__tests__/getMedianOfCodeErrors.test.ts
@@ -0,0 +1,69 @@
+import { QuaggaJSResultObject } from "@ericblade/quagga2";
+import { getMedianOfCodeErrors } from "../getMedianOfCodeErrors";
+
+export const createMockQuaggaResult = (
+ decodedCodes: Array<{
+ error?: number;
+ code: number;
+ start: number;
+ end: number;
+ }> = []
+): QuaggaJSResultObject => ({
+ codeResult: {
+ code: "123456",
+ start: 0,
+ end: 10,
+ codeset: 1,
+ startInfo: {
+ error: 0,
+ code: 1,
+ start: 0,
+ end: 5,
+ },
+ decodedCodes,
+ endInfo: {
+ error: 0,
+ code: 1,
+ start: 5,
+ end: 10,
+ },
+ direction: 1,
+ format: "QR",
+ },
+ barcodes: [],
+ line: [{ x: 0, y: 0 }],
+ angle: 0,
+ pattern: [],
+ box: [],
+ boxes: [],
+});
+
+describe("getMedianOfCodeErrors", () => {
+ it("should return the median of code errors when there are valid errors", () => {
+ const result = createMockQuaggaResult([
+ { error: 1, code: 1, start: 0, end: 5 },
+ { error: 3, code: 1, start: 5, end: 10 },
+ { error: 2, code: 1, start: 0, end: 10 },
+ ]);
+
+ const median = getMedianOfCodeErrors(result);
+ expect(median).toBe(2); // The median of [1, 3, 2] is 2
+ });
+
+ it("should return undefined when there are no code errors", () => {
+ const result = createMockQuaggaResult([
+ { code: 1, start: 0, end: 5 }, // No error property
+ { code: 1, start: 5, end: 10 },
+ ]);
+
+ const median = getMedianOfCodeErrors(result);
+ expect(median).toBeUndefined();
+ });
+
+ it("should handle an empty decodedCodes array", () => {
+ const result = createMockQuaggaResult([]);
+
+ const median = getMedianOfCodeErrors(result);
+ expect(median).toBeUndefined();
+ });
+});
diff --git a/frontend/src/shared/utils/__tests__/useAuthentication.test.tsx b/frontend/src/shared/utils/__tests__/useAuthentication.test.tsx
new file mode 100644
index 0000000..0f85e3e
--- /dev/null
+++ b/frontend/src/shared/utils/__tests__/useAuthentication.test.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import { render, waitFor, cleanup } from "@testing-library/react";
+import { AuthContext } from "../../../App";
+import { useAuth } from "../useAuthentication";
+
+const MockedComponent: React.FC = () => {
+ const { authenticated } = useAuth();
+ return
{authenticated}
;
+};
+
+let authenticated = false;
+const setAuthenticated = (args: boolean) => (authenticated = args);
+
+describe("useAuth", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ cleanup();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ cleanup();
+ });
+
+ it("should set authenticated to true when fetch returns ok", async () => {
+ global.fetch = jest.fn(() => Promise.resolve({ ok: true })) as jest.Mock;
+
+ render(
+
+
+
+ );
+
+ await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
+
+ expect(authenticated).toBe(true);
+ });
+
+ it("should set authenticated to false when fetch returns not ok", async () => {
+ global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock;
+
+ render(
+
+
+
+ );
+
+ await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
+
+ expect(authenticated).toBe(false);
+ });
+
+ it("should call fetch once", async () => {
+ global.fetch = jest.fn(() => Promise.resolve({ ok: false })) as jest.Mock;
+
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/frontend/src/shared/utils/drawQuaggaProcessing.ts b/frontend/src/shared/utils/drawQuaggaProcessing.ts
new file mode 100644
index 0000000..6185a7b
--- /dev/null
+++ b/frontend/src/shared/utils/drawQuaggaProcessing.ts
@@ -0,0 +1,34 @@
+import Quagga, { QuaggaJSResultObject } from "@ericblade/quagga2";
+
+export const drawQuaggaProcessing = (result: QuaggaJSResultObject) => {
+ const drawingCtx = Quagga.canvas.ctx.overlay;
+ const drawingCanvas = Quagga.canvas.dom.overlay;
+ drawingCtx.font = "24px Arial";
+ drawingCtx.fillStyle = "green";
+ const width = drawingCanvas.getAttribute("width");
+ const height = drawingCanvas.getAttribute("height");
+
+ if (result) {
+ if (result.boxes && width && height) {
+ drawingCtx.clearRect(0, 0, parseInt(width), parseInt(height));
+ result.boxes
+ .filter((box) => box !== result.box)
+ .forEach((box) => {
+ Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
+ color: "purple",
+ lineWidth: 2,
+ });
+ });
+ }
+ if (result.box) {
+ Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
+ color: "blue",
+ lineWidth: 2,
+ });
+ }
+ if (result.codeResult && result.codeResult.code) {
+ drawingCtx.font = "24px Arial";
+ drawingCtx.fillText(result.codeResult.code, 10, 20);
+ }
+ }
+};
diff --git a/frontend/src/shared/utils/getMedianOfCodeErrors.ts b/frontend/src/shared/utils/getMedianOfCodeErrors.ts
new file mode 100644
index 0000000..e43e0ca
--- /dev/null
+++ b/frontend/src/shared/utils/getMedianOfCodeErrors.ts
@@ -0,0 +1,20 @@
+import { QuaggaJSResultObject } from "@ericblade/quagga2";
+
+export const getMedianOfCodeErrors = (result: QuaggaJSResultObject) => {
+ const errors = result.codeResult.decodedCodes.flatMap((x) => x.error);
+ const medianOfErrors = getMedian(errors);
+ return medianOfErrors;
+};
+
+export const getMedian = (arr: (number | undefined)[]) => {
+ const newArr = [...arr].filter((x) => typeof x !== "undefined");
+ if (!newArr.length) {
+ return;
+ }
+ newArr.sort((a, b) => a! - b!);
+ const half = Math.floor(newArr.length / 2);
+ if (newArr.length % 2 === 1) {
+ return newArr[half];
+ }
+ return (newArr[half - 1]! + newArr[half]!) / 2;
+};
diff --git a/frontend/src/shared/utils/useAuthentication.ts b/frontend/src/shared/utils/useAuthentication.ts
index b3d5c36..3d6e203 100644
--- a/frontend/src/shared/utils/useAuthentication.ts
+++ b/frontend/src/shared/utils/useAuthentication.ts
@@ -1,11 +1,10 @@
-import { useCallback, useContext } from "react";
+import { useCallback, useContext, useEffect } from "react";
import { AuthContext } from "../../App";
-import { useQuery } from "@tanstack/react-query";
type UseAuthHook = { authenticated: boolean };
export const useAuth = (): UseAuthHook => {
- const auth = useContext(AuthContext);
+ const { authenticated, setAuthenticated } = useContext(AuthContext);
const authenticationCheck = useCallback(
async () =>
@@ -16,17 +15,14 @@ export const useAuth = (): UseAuthHook => {
"Content-Type": "application/json",
},
}).then((res) => {
- auth.setAuthenticated?.(res.ok);
- //react-query can't handle undefined without throwing a warning ...
- return null;
+ setAuthenticated?.(res.ok);
}),
- [auth]
+ [setAuthenticated]
);
- useQuery({
- queryFn: authenticationCheck,
- queryKey: ["auth"],
- });
+ useEffect(() => {
+ authenticationCheck();
+ }, [authenticationCheck]);
- return auth;
+ return { authenticated };
};
diff --git a/frontend/src/shared/utils/useScanner.ts b/frontend/src/shared/utils/useScanner.ts
index 70666b2..44aa7ee 100644
--- a/frontend/src/shared/utils/useScanner.ts
+++ b/frontend/src/shared/utils/useScanner.ts
@@ -3,6 +3,8 @@ import Quagga, {
QuaggaJSResultObject,
} from "@ericblade/quagga2";
import { useState, useCallback, useEffect } from "react";
+import { getMedianOfCodeErrors } from "./getMedianOfCodeErrors";
+import { drawQuaggaProcessing } from "./drawQuaggaProcessing";
const constraints = {
width: 640,
@@ -29,28 +31,6 @@ export const useScanner = ({
const [scannerRef, setScannerRef] = useState(null);
- const getMedian = useCallback((arr: (number | undefined)[]) => {
- const newArr = [...arr].filter((x) => typeof x !== "undefined");
- if (!newArr.length) {
- return;
- }
- newArr.sort((a, b) => a! - b!);
- const half = Math.floor(newArr.length / 2);
- if (newArr.length % 2 === 1) {
- return newArr[half];
- }
- return (newArr[half - 1]! + newArr[half]!) / 2;
- }, []);
-
- const getMedianOfCodeErrors = useCallback(
- (result: QuaggaJSResultObject) => {
- const errors = result.codeResult.decodedCodes.flatMap((x) => x.error);
- const medianOfErrors = getMedian(errors);
- return medianOfErrors;
- },
- [getMedian]
- );
-
const errorCheck = useCallback(
(result: QuaggaJSResultObject) => {
if (!onDetected) {
@@ -66,42 +46,9 @@ export const useScanner = ({
onDetected(result.codeResult.code);
}
},
- [getMedianOfCodeErrors, onDetected]
+ [onDetected]
);
- const handleProcessed = (result: QuaggaJSResultObject) => {
- const drawingCtx = Quagga.canvas.ctx.overlay;
- const drawingCanvas = Quagga.canvas.dom.overlay;
- drawingCtx.font = "24px Arial";
- drawingCtx.fillStyle = "green";
- const width = drawingCanvas.getAttribute("width");
- const height = drawingCanvas.getAttribute("height");
-
- if (result) {
- if (result.boxes && width && height) {
- drawingCtx.clearRect(0, 0, parseInt(width), parseInt(height));
- result.boxes
- .filter((box) => box !== result.box)
- .forEach((box) => {
- Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
- color: "purple",
- lineWidth: 2,
- });
- });
- }
- if (result.box) {
- Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
- color: "blue",
- lineWidth: 2,
- });
- }
- if (result.codeResult && result.codeResult.code) {
- drawingCtx.font = "24px Arial";
- drawingCtx.fillText(result.codeResult.code, 10, 20);
- }
- }
- };
-
const getCameraId = useCallback(async () => {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
@@ -150,7 +97,7 @@ export const useScanner = ({
locate: true,
},
async (err) => {
- Quagga.onProcessed(handleProcessed);
+ Quagga.onProcessed(drawQuaggaProcessing);
if (err) {
return console.error("Error starting Quagga:", err);
@@ -170,7 +117,7 @@ export const useScanner = ({
await Quagga.CameraAccess.release();
await Quagga.stop();
Quagga.offDetected(errorCheck);
- Quagga.offProcessed(handleProcessed);
+ Quagga.offProcessed(drawQuaggaProcessing);
}, [errorCheck]);
useEffect(() => {