testing(utils): add test for auth and scanner

This commit is contained in:
0ry5 2024-10-21 20:31:21 +02:00
parent 118ca1ed82
commit fac5e7b66c
9 changed files with 238 additions and 88 deletions

View file

@ -9,7 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ericblade/quagga2": "^1.8.4", "@ericblade/quagga2": "^1.8.4",
"@tanstack/react-query": "^5.52.1", "@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
@ -22,6 +22,7 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"history": "^5.3.0", "history": "^5.3.0",
"jest-canvas-mock": "^2.5.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -3727,9 +3728,9 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.56.2", "version": "5.59.13",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz",
"integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==", "integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@ -3737,12 +3738,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.56.2", "version": "5.59.15",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz",
"integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==", "integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.56.2" "@tanstack/query-core": "5.59.13"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -6655,6 +6656,12 @@
"node": ">=4" "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": { "node_modules/cssnano": {
"version": "5.1.15", "version": "5.1.15",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", "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": { "node_modules/jest-changed-files": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
@ -12147,6 +12164,15 @@
"mkdirp": "bin/cmd.js" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -5,7 +5,7 @@
"proxy": "http://localhost:3001", "proxy": "http://localhost:3001",
"dependencies": { "dependencies": {
"@ericblade/quagga2": "^1.8.4", "@ericblade/quagga2": "^1.8.4",
"@tanstack/react-query": "^5.52.1", "@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
@ -18,6 +18,7 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"fast-xml-parser": "^4.5.0", "fast-xml-parser": "^4.5.0",
"history": "^5.3.0", "history": "^5.3.0",
"jest-canvas-mock": "^2.5.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View file

@ -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(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -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();
});
});

View file

@ -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 <div>{authenticated}</div>;
};
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(
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
<MockedComponent />
</AuthContext.Provider>
);
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(
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
<MockedComponent />
</AuthContext.Provider>
);
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(
<AuthContext.Provider value={{ authenticated, setAuthenticated }}>
<MockedComponent />
</AuthContext.Provider>
);
await waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -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);
}
}
};

View file

@ -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;
};

View file

@ -1,11 +1,10 @@
import { useCallback, useContext } from "react"; import { useCallback, useContext, useEffect } from "react";
import { AuthContext } from "../../App"; import { AuthContext } from "../../App";
import { useQuery } from "@tanstack/react-query";
type UseAuthHook = { authenticated: boolean }; type UseAuthHook = { authenticated: boolean };
export const useAuth = (): UseAuthHook => { export const useAuth = (): UseAuthHook => {
const auth = useContext(AuthContext); const { authenticated, setAuthenticated } = useContext(AuthContext);
const authenticationCheck = useCallback( const authenticationCheck = useCallback(
async () => async () =>
@ -16,17 +15,14 @@ export const useAuth = (): UseAuthHook => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}).then((res) => { }).then((res) => {
auth.setAuthenticated?.(res.ok); setAuthenticated?.(res.ok);
//react-query can't handle undefined without throwing a warning ...
return null;
}), }),
[auth] [setAuthenticated]
); );
useQuery({ useEffect(() => {
queryFn: authenticationCheck, authenticationCheck();
queryKey: ["auth"], }, [authenticationCheck]);
});
return auth; return { authenticated };
}; };

View file

@ -3,6 +3,8 @@ import Quagga, {
QuaggaJSResultObject, QuaggaJSResultObject,
} from "@ericblade/quagga2"; } from "@ericblade/quagga2";
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import { getMedianOfCodeErrors } from "./getMedianOfCodeErrors";
import { drawQuaggaProcessing } from "./drawQuaggaProcessing";
const constraints = { const constraints = {
width: 640, width: 640,
@ -29,28 +31,6 @@ export const useScanner = ({
const [scannerRef, setScannerRef] = useState<HTMLDivElement | null>(null); const [scannerRef, setScannerRef] = useState<HTMLDivElement | null>(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( const errorCheck = useCallback(
(result: QuaggaJSResultObject) => { (result: QuaggaJSResultObject) => {
if (!onDetected) { if (!onDetected) {
@ -66,42 +46,9 @@ export const useScanner = ({
onDetected(result.codeResult.code); 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 getCameraId = useCallback(async () => {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" }, video: { facingMode: "environment" },
@ -150,7 +97,7 @@ export const useScanner = ({
locate: true, locate: true,
}, },
async (err) => { async (err) => {
Quagga.onProcessed(handleProcessed); Quagga.onProcessed(drawQuaggaProcessing);
if (err) { if (err) {
return console.error("Error starting Quagga:", err); return console.error("Error starting Quagga:", err);
@ -170,7 +117,7 @@ export const useScanner = ({
await Quagga.CameraAccess.release(); await Quagga.CameraAccess.release();
await Quagga.stop(); await Quagga.stop();
Quagga.offDetected(errorCheck); Quagga.offDetected(errorCheck);
Quagga.offProcessed(handleProcessed); Quagga.offProcessed(drawQuaggaProcessing);
}, [errorCheck]); }, [errorCheck]);
useEffect(() => { useEffect(() => {