From b6506435d5036a79befd63e7608e737550c81cca Mon Sep 17 00:00:00 2001 From: Emma781227 Date: Mon, 20 Jul 2026 10:35:35 +0200 Subject: [PATCH] test --- frontend/package-lock.json | 1144 ++++++++++++++++- frontend/package.json | 11 +- frontend/src/setupTests.js | 8 + .../src/test/fonctionnel/Connexion.test.jsx | 84 ++ .../test/fonctionnel/CookieBanner.test.jsx | 39 + .../src/test/fonctionnel/Inscription.test.jsx | 120 ++ .../test/fonctionnel/ProtectedRoute.test.jsx | 80 ++ .../fonctionnel/SaisirCodeUnique.test.jsx | 93 ++ frontend/src/test/renderWithProviders.jsx | 29 + frontend/src/test/unit/AuthContext.test.jsx | 71 + frontend/src/test/unit/auth.service.test.js | 102 ++ frontend/src/test/unit/game.service.test.js | 68 + .../src/test/unit/useHandleAuthError.test.jsx | 51 + frontend/vite.config.js | 5 + 14 files changed, 1902 insertions(+), 3 deletions(-) create mode 100644 frontend/src/setupTests.js create mode 100644 frontend/src/test/fonctionnel/Connexion.test.jsx create mode 100644 frontend/src/test/fonctionnel/CookieBanner.test.jsx create mode 100644 frontend/src/test/fonctionnel/Inscription.test.jsx create mode 100644 frontend/src/test/fonctionnel/ProtectedRoute.test.jsx create mode 100644 frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx create mode 100644 frontend/src/test/renderWithProviders.jsx create mode 100644 frontend/src/test/unit/AuthContext.test.jsx create mode 100644 frontend/src/test/unit/auth.service.test.js create mode 100644 frontend/src/test/unit/game.service.test.js create mode 100644 frontend/src/test/unit/useHandleAuthError.test.jsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a814f40..b475800 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,9 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -22,10 +25,70 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "jsdom": "^29.1.1", "tailwindcss": "^4.3.0", - "vite": "^8.0.12" + "vite": "^8.0.12", + "vitest": "^4.1.10" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -219,6 +282,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -267,6 +340,161 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -406,6 +634,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -849,6 +1095,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -1121,6 +1374,96 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1132,6 +1475,31 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1170,6 +1538,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -1200,6 +1569,119 @@ } } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1241,6 +1723,49 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1264,6 +1789,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -1333,6 +1868,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1368,6 +1913,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1375,6 +1941,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1393,6 +1973,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1400,6 +1987,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1410,6 +2007,13 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.371", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", @@ -1431,6 +2035,26 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1627,6 +2251,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1637,6 +2271,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1802,6 +2446,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1822,6 +2479,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1845,6 +2512,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1869,6 +2543,58 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2227,6 +2953,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2237,6 +2973,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2296,6 +3049,20 @@ "node": ">=18" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2346,6 +3113,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2366,6 +3146,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2426,6 +3213,21 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2459,6 +3261,13 @@ "react": "^19.2.7" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", @@ -2497,6 +3306,30 @@ "react-dom": ">=18" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -2531,6 +3364,19 @@ "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2576,6 +3422,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2586,6 +3439,40 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -2607,6 +3494,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2624,6 +3528,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2645,6 +3605,16 @@ "node": ">= 0.8.0" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2765,6 +3735,144 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2781,6 +3889,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2791,6 +3916,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 83f90e6..5fc8338 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "react": "^19.2.6", @@ -17,6 +19,9 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -24,7 +29,9 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "jsdom": "^29.1.1", "tailwindcss": "^4.3.0", - "vite": "^8.0.12" + "vite": "^8.0.12", + "vitest": "^4.1.10" } } diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 0000000..d0879b7 --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1,8 @@ +import '@testing-library/jest-dom' +import { afterEach } from 'vitest' +import { cleanup } from '@testing-library/react' + +afterEach(() => { + cleanup() + localStorage.clear() +}) diff --git a/frontend/src/test/fonctionnel/Connexion.test.jsx b/frontend/src/test/fonctionnel/Connexion.test.jsx new file mode 100644 index 0000000..76234a3 --- /dev/null +++ b/frontend/src/test/fonctionnel/Connexion.test.jsx @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import Connexion from '../../pages/Connexion' +import { renderWithProviders } from '../renderWithProviders' +import { loginApi } from '../../services/auth' + +// On mocke le service d'API : les tests de composant ne font pas de vraies requêtes HTTP. +vi.mock('../../services/auth', () => ({ + loginApi: vi.fn(), +})) + +describe('Connexion', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('affiche le formulaire de connexion', () => { + renderWithProviders(, { route: '/connexion' }) + + expect(screen.getByRole('heading', { name: /se connecter/i })).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: /se connecter/i })).toBeInTheDocument() + }) + + it('affiche le message de succès après une inscription', () => { + renderWithProviders(, { + route: { pathname: '/connexion', state: { registered: true } }, + }) + + expect(screen.getByText(/compte créé avec succès/i)).toBeInTheDocument() + }) + + it("affiche l'erreur renvoyée par l'API en cas d'échec", async () => { + loginApi.mockRejectedValue(new Error('Email ou mot de passe incorrect.')) + + renderWithProviders(, { route: '/connexion' }) + + await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'test@example.com') + await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'mauvais-mdp') + await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) + + expect(await screen.findByText('Email ou mot de passe incorrect.')).toBeInTheDocument() + expect(loginApi).toHaveBeenCalledWith({ email: 'test@example.com', password: 'mauvais-mdp' }) + }) + + it('connecte un client et redirige vers /mes-gains', async () => { + loginApi.mockResolvedValue({ + token: 'jwt-token', + user: { id: 1, email: 'client@example.com', role: 'CLIENT' }, + }) + + renderWithProviders(, { + route: '/connexion', + routes: { '/mes-gains':

Page Mes Gains

}, + }) + + await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'client@example.com') + await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') + await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) + + expect(await screen.findByText('Page Mes Gains')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBe('jwt-token') + }) + + it("connecte un admin et redirige vers /admin", async () => { + loginApi.mockResolvedValue({ + token: 'jwt-token', + user: { id: 2, email: 'admin@example.com', role: 'ADMIN' }, + }) + + renderWithProviders(, { + route: '/connexion', + routes: { '/admin':

Page Admin

}, + }) + + await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'admin@example.com') + await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') + await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) + + expect(await screen.findByText('Page Admin')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/test/fonctionnel/CookieBanner.test.jsx b/frontend/src/test/fonctionnel/CookieBanner.test.jsx new file mode 100644 index 0000000..2e4a139 --- /dev/null +++ b/frontend/src/test/fonctionnel/CookieBanner.test.jsx @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import CookieBanner from '../../components/CookieBanner' +import { renderWithProviders } from '../renderWithProviders' + +describe('CookieBanner', () => { + it("s'affiche quand aucun choix n'a été fait", () => { + renderWithProviders() + + expect(screen.getByText(/ce site utilise des cookies/i)).toBeInTheDocument() + }) + + it('enregistre le consentement et disparaît quand on accepte', async () => { + renderWithProviders() + + await userEvent.click(screen.getByRole('button', { name: /accepter/i })) + + expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument() + expect(localStorage.getItem('cookieConsent')).toBe('accepted') + }) + + it('enregistre le refus et disparaît quand on refuse', async () => { + renderWithProviders() + + await userEvent.click(screen.getByRole('button', { name: /refuser/i })) + + expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument() + expect(localStorage.getItem('cookieConsent')).toBe('refused') + }) + + it("ne s'affiche pas si un choix a déjà été fait", () => { + localStorage.setItem('cookieConsent', 'accepted') + + renderWithProviders() + + expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/test/fonctionnel/Inscription.test.jsx b/frontend/src/test/fonctionnel/Inscription.test.jsx new file mode 100644 index 0000000..efac812 --- /dev/null +++ b/frontend/src/test/fonctionnel/Inscription.test.jsx @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import Inscription from '../../pages/Inscription' +import { renderWithProviders } from '../renderWithProviders' +import { registerApi, verifyCodeApi } from '../../services/auth' + +vi.mock('../../services/auth', () => ({ + registerApi: vi.fn(), + verifyCodeApi: vi.fn(), +})) + +// Remplit et soumet le formulaire d'inscription (étape 1). +async function remplirEtSoumettre() { + await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'jean@example.com') + await userEvent.type(screen.getByPlaceholderText(/votre nom/i), 'Dupont') + await userEvent.type(screen.getByPlaceholderText(/votre prénom/i), 'Jean') + await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!') + await userEvent.click(screen.getByRole('checkbox')) + await userEvent.click(screen.getByRole('button', { name: /s'inscrire/i })) +} + +// Saisit le code à 4 chiffres de l'étape de vérification. +async function saisirCode(code) { + const inputs = screen.getAllByRole('textbox') + for (let i = 0; i < 4; i++) { + await userEvent.type(inputs[i], code[i]) + } +} + +describe('Inscription', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("affiche le formulaire d'inscription", () => { + renderWithProviders(, { route: '/inscription' }) + + expect(screen.getByRole('heading', { name: /s'inscrire/i })).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre nom/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre prénom/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument() + }) + + it("désactive le bouton tant que les conditions ne sont pas acceptées", async () => { + renderWithProviders(, { route: '/inscription' }) + + const bouton = screen.getByRole('button', { name: /s'inscrire/i }) + expect(bouton).toBeDisabled() + + await userEvent.click(screen.getByRole('checkbox')) + expect(bouton).toBeEnabled() + }) + + it("passe à l'étape de vérification après une inscription réussie", async () => { + registerApi.mockResolvedValue({ message: 'ok' }) + + renderWithProviders(, { route: '/inscription' }) + await remplirEtSoumettre() + + expect(registerApi).toHaveBeenCalledWith({ + email: 'jean@example.com', + firstname: 'Jean', + lastname: 'Dupont', + password: 'MotDePasse123!', + }) + expect(await screen.findByRole('heading', { name: /vérifiez votre email/i })).toBeInTheDocument() + expect(screen.getByText('jean@example.com')).toBeInTheDocument() + }) + + it("affiche l'erreur de l'API si l'inscription échoue", async () => { + registerApi.mockRejectedValue(new Error('Cet email est déjà utilisé.')) + + renderWithProviders(, { route: '/inscription' }) + await remplirEtSoumettre() + + expect(await screen.findByText('Cet email est déjà utilisé.')).toBeInTheDocument() + expect(screen.queryByRole('heading', { name: /vérifiez votre email/i })).not.toBeInTheDocument() + }) + + it('connecte et redirige vers /saisir-code-unique quand le code est valide', async () => { + registerApi.mockResolvedValue({ message: 'ok' }) + verifyCodeApi.mockResolvedValue({ + token: 'jwt-token', + user: { id: 1, email: 'jean@example.com', role: 'CLIENT' }, + }) + + renderWithProviders(, { + route: '/inscription', + routes: { '/saisir-code-unique':

Page Saisir Code

}, + }) + await remplirEtSoumettre() + await screen.findByRole('heading', { name: /vérifiez votre email/i }) + + await saisirCode('1234') + await userEvent.click(screen.getByRole('button', { name: /valider le code/i })) + + expect(verifyCodeApi).toHaveBeenCalledWith({ email: 'jean@example.com', code: '1234' }) + expect(await screen.findByText('Page Saisir Code')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBe('jwt-token') + }) + + it("affiche l'erreur et vide les champs quand le code est invalide", async () => { + registerApi.mockResolvedValue({ message: 'ok' }) + verifyCodeApi.mockRejectedValue(new Error('Code invalide.')) + + renderWithProviders(, { route: '/inscription' }) + await remplirEtSoumettre() + await screen.findByRole('heading', { name: /vérifiez votre email/i }) + + await saisirCode('0000') + await userEvent.click(screen.getByRole('button', { name: /valider le code/i })) + + expect(await screen.findByText('Code invalide.')).toBeInTheDocument() + for (const input of screen.getAllByRole('textbox')) { + expect(input).toHaveValue('') + } + }) +}) diff --git a/frontend/src/test/fonctionnel/ProtectedRoute.test.jsx b/frontend/src/test/fonctionnel/ProtectedRoute.test.jsx new file mode 100644 index 0000000..b33fc34 --- /dev/null +++ b/frontend/src/test/fonctionnel/ProtectedRoute.test.jsx @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import ProtectedRoute from '../../components/ProtectedRoute' +import { renderWithProviders } from '../renderWithProviders' + +describe('ProtectedRoute', () => { + it("redirige vers /connexion si l'utilisateur n'est pas connecté", () => { + renderWithProviders( + +

Contenu protégé

+
, + { + route: '/mes-gains', + routes: { '/connexion':

Page Connexion

}, + } + ) + + expect(screen.getByText('Page Connexion')).toBeInTheDocument() + expect(screen.queryByText('Contenu protégé')).not.toBeInTheDocument() + }) + + it("affiche le contenu si l'utilisateur est connecté", () => { + renderWithProviders( + +

Contenu protégé

+
, + { + route: '/mes-gains', + user: { id: 1, email: 'client@example.com', role: 'CLIENT' }, + } + ) + + expect(screen.getByText('Contenu protégé')).toBeInTheDocument() + }) + + it('affiche le contenu si le rôle est autorisé', () => { + renderWithProviders( + +

Espace admin

+
, + { + route: '/admin', + user: { id: 2, email: 'admin@example.com', role: 'ADMIN' }, + } + ) + + expect(screen.getByText('Espace admin')).toBeInTheDocument() + }) + + it("redirige un client vers son espace s'il tente d'accéder à l'admin", () => { + renderWithProviders( + +

Espace admin

+
, + { + route: '/admin', + user: { id: 1, email: 'client@example.com', role: 'CLIENT' }, + routes: { '/mes-gains':

Page Mes Gains

}, + } + ) + + expect(screen.getByText('Page Mes Gains')).toBeInTheDocument() + expect(screen.queryByText('Espace admin')).not.toBeInTheDocument() + }) + + it("redirige un employé vers son espace s'il tente d'accéder à l'admin", () => { + renderWithProviders( + +

Espace admin

+
, + { + route: '/admin', + user: { id: 3, email: 'employe@example.com', role: 'EMPLOYEE' }, + routes: { '/employe':

Page Employé

}, + } + ) + + expect(screen.getByText('Page Employé')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx b/frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx new file mode 100644 index 0000000..8b6c5fe --- /dev/null +++ b/frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx @@ -0,0 +1,93 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import SaisirCodeUnique from '../../pages/SaisirCodeUnique' +import Perdu from '../../pages/Perdu' +import { renderWithProviders } from '../renderWithProviders' +import { participateApi } from '../../services/game' + +vi.mock('../../services/game', () => ({ + participateApi: vi.fn(), +})) + +const client = { id: 1, email: 'client@example.com', role: 'CLIENT' } + +async function saisirEtValider(code) { + await userEvent.type(screen.getByPlaceholderText(/votre code/i), code) + await userEvent.click(screen.getByRole('button', { name: /valider/i })) +} + +describe('SaisirCodeUnique', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('désactive le bouton tant que le code ne fait pas 10 caractères', async () => { + renderWithProviders(, { route: '/saisir-code-unique', user: client }) + + const bouton = screen.getByRole('button', { name: /valider/i }) + expect(bouton).toBeDisabled() + + await userEvent.type(screen.getByPlaceholderText(/votre code/i), 'ABCDE12345') + expect(bouton).toBeEnabled() + }) + + it('met le code en majuscules et l\'envoie à l\'API', async () => { + participateApi.mockResolvedValue({ gain: { name: 'Thé détox' }, claim: { id: 1 } }) + + renderWithProviders(, { route: '/saisir-code-unique', user: client }) + await saisirEtValider('abcde12345') + + expect(participateApi).toHaveBeenCalledWith('ABCDE12345') + }) + + it("affiche l'écran Félicitation puis le détail du lot quand le code est gagnant", async () => { + participateApi.mockResolvedValue({ + gain: { name: 'Thé signature', description: 'Un thé d\'exception', value: 39 }, + claim: { id: 1 }, + }) + + renderWithProviders(, { route: '/saisir-code-unique', user: client }) + await saisirEtValider('ABCDE12345') + + // Overlay de félicitation + expect(await screen.findByText(/félicitation/i)).toBeInTheDocument() + expect(screen.getByText('Thé signature')).toBeInTheDocument() + + // Détail du lot + await userEvent.click(screen.getByRole('button', { name: /découvrir mon lot/i })) + expect(screen.getByText(/valeur estimée/i)).toBeInTheDocument() + expect(screen.getByText('39 €')).toBeInTheDocument() + expect(screen.getByText(/en attente de remise/i)).toBeInTheDocument() + }) + + it("redirige vers /perdu avec le message de l'API quand le code est invalide", async () => { + participateApi.mockRejectedValue(new Error('Ce code a déjà été utilisé.')) + + renderWithProviders(, { + route: '/saisir-code-unique', + user: client, + routes: { '/perdu': }, + }) + await saisirEtValider('ABCDE12345') + + expect(await screen.findByText(/réessayer la prochaine fois/i)).toBeInTheDocument() + expect(screen.getByText('Ce code a déjà été utilisé.')).toBeInTheDocument() + }) + + it('déconnecte et redirige vers /connexion si la session a expiré (401)', async () => { + participateApi.mockRejectedValue( + Object.assign(new Error('Session expirée'), { status: 401 }) + ) + + renderWithProviders(, { + route: '/saisir-code-unique', + user: client, + routes: { '/connexion':

Page Connexion

}, + }) + await saisirEtValider('ABCDE12345') + + expect(await screen.findByText('Page Connexion')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBeNull() + }) +}) diff --git a/frontend/src/test/renderWithProviders.jsx b/frontend/src/test/renderWithProviders.jsx new file mode 100644 index 0000000..a5a2668 --- /dev/null +++ b/frontend/src/test/renderWithProviders.jsx @@ -0,0 +1,29 @@ +import { render } from '@testing-library/react' +import { MemoryRouter, Routes, Route } from 'react-router-dom' +import { AuthProvider } from '../context/AuthContext' + +// Rend un composant avec les mêmes providers que l'appli (AuthContext + Router). +// - `route` : URL initiale (peut être un objet { pathname, state }) +// - `user` : utilisateur pré-connecté (écrit dans localStorage avant le rendu, +// comme le ferait une vraie session) +// - `routes`: routes supplémentaires pour vérifier les redirections, +// ex: { '/mes-gains':

Page Mes Gains

} +export function renderWithProviders(ui, { route = '/', user = null, routes = {} } = {}) { + if (user) { + localStorage.setItem('user', JSON.stringify(user)) + localStorage.setItem('token', 'fake-token') + } + + return render( + + + + + {Object.entries(routes).map(([path, element]) => ( + + ))} + + + + ) +} diff --git a/frontend/src/test/unit/AuthContext.test.jsx b/frontend/src/test/unit/AuthContext.test.jsx new file mode 100644 index 0000000..ff8f84f --- /dev/null +++ b/frontend/src/test/unit/AuthContext.test.jsx @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { AuthProvider, useAuth } from '../../context/AuthContext' + +// Petit composant de test pour manipuler le contexte comme le ferait une page. +function Consumer() { + const { user, login, logout } = useAuth() + return ( +
+

{user ? `Connecté : ${user.email}` : 'Déconnecté'}

+ + +
+ ) +} + +describe('AuthContext', () => { + it("n'a pas d'utilisateur par défaut", () => { + render( + + + + ) + expect(screen.getByText('Déconnecté')).toBeInTheDocument() + }) + + it('restaure la session depuis le localStorage', () => { + localStorage.setItem('user', JSON.stringify({ email: 'deja@la.fr', role: 'CLIENT' })) + + render( + + + + ) + expect(screen.getByText('Connecté : deja@la.fr')).toBeInTheDocument() + }) + + it('login met à jour le user et écrit token + user dans le localStorage', async () => { + render( + + + + ) + + await userEvent.click(screen.getByRole('button', { name: 'se connecter' })) + + expect(screen.getByText('Connecté : a@b.fr')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBe('mon-jwt') + expect(JSON.parse(localStorage.getItem('user'))).toEqual({ email: 'a@b.fr', role: 'CLIENT' }) + }) + + it('logout vide le user et le localStorage', async () => { + localStorage.setItem('token', 'mon-jwt') + localStorage.setItem('user', JSON.stringify({ email: 'a@b.fr', role: 'CLIENT' })) + + render( + + + + ) + + await userEvent.click(screen.getByRole('button', { name: 'se déconnecter' })) + + expect(screen.getByText('Déconnecté')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBeNull() + expect(localStorage.getItem('user')).toBeNull() + }) +}) diff --git a/frontend/src/test/unit/auth.service.test.js b/frontend/src/test/unit/auth.service.test.js new file mode 100644 index 0000000..561e909 --- /dev/null +++ b/frontend/src/test/unit/auth.service.test.js @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { loginApi, registerApi, verifyCodeApi, changePasswordApi } from '../../services/auth' + +// Tests unitaires du service auth : on mocke fetch, aucune vraie requête HTTP. +const mockFetch = vi.fn() + +function jsonResponse(body, { ok = true, status = 200 } = {}) { + return Promise.resolve({ ok, status, json: () => Promise.resolve(body) }) +} + +beforeEach(() => { + vi.stubGlobal('fetch', mockFetch) + mockFetch.mockReset() +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('loginApi', () => { + it('envoie email et mot de passe et renvoie les données', async () => { + const payload = { token: 'jwt', user: { email: 'a@b.fr', role: 'CLIENT' } } + mockFetch.mockReturnValue(jsonResponse(payload)) + + const data = await loginApi({ email: 'a@b.fr', password: 'secret' }) + + expect(data).toEqual(payload) + expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.fr', password: 'secret' }), + }) + }) + + it("lève une erreur avec le message et le status de l'API en cas d'échec", async () => { + mockFetch.mockReturnValue( + jsonResponse({ message: 'Identifiants invalides.' }, { ok: false, status: 401 }) + ) + + await expect(loginApi({ email: 'a@b.fr', password: 'faux' })).rejects.toMatchObject({ + message: 'Identifiants invalides.', + status: 401, + }) + }) + + it("utilise un message par défaut si l'API n'en renvoie pas", async () => { + mockFetch.mockReturnValue(jsonResponse({}, { ok: false, status: 500 })) + + await expect(loginApi({ email: 'a@b.fr', password: 'x' })).rejects.toThrow( + 'Erreur de connexion.' + ) + }) +}) + +describe('registerApi', () => { + it("envoie les informations d'inscription", async () => { + mockFetch.mockReturnValue(jsonResponse({ message: 'ok' })) + + await registerApi({ email: 'a@b.fr', firstname: 'Jean', lastname: 'Dupont', password: 'secret' }) + + expect(mockFetch).toHaveBeenCalledWith('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'a@b.fr', firstname: 'Jean', lastname: 'Dupont', password: 'secret' }), + }) + }) +}) + +describe('verifyCodeApi', () => { + it('renvoie le token et le user quand le code est valide', async () => { + const payload = { token: 'jwt', user: { email: 'a@b.fr' } } + mockFetch.mockReturnValue(jsonResponse(payload)) + + const data = await verifyCodeApi({ email: 'a@b.fr', code: '1234' }) + + expect(data).toEqual(payload) + }) + + it('lève "Code invalide." par défaut en cas d\'échec', async () => { + mockFetch.mockReturnValue(jsonResponse({}, { ok: false, status: 400 })) + + await expect(verifyCodeApi({ email: 'a@b.fr', code: '0000' })).rejects.toThrow('Code invalide.') + }) +}) + +describe('changePasswordApi', () => { + it('envoie le token du localStorage dans le header Authorization', async () => { + localStorage.setItem('token', 'mon-jwt') + mockFetch.mockReturnValue(jsonResponse({ message: 'ok' })) + + await changePasswordApi({ currentPassword: 'ancien', newPassword: 'nouveau' }) + + expect(mockFetch).toHaveBeenCalledWith('/api/auth/change-password', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer mon-jwt', + }, + body: JSON.stringify({ currentPassword: 'ancien', newPassword: 'nouveau' }), + }) + }) +}) diff --git a/frontend/src/test/unit/game.service.test.js b/frontend/src/test/unit/game.service.test.js new file mode 100644 index 0000000..3904603 --- /dev/null +++ b/frontend/src/test/unit/game.service.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { myGainsApi, participateApi } from '../../services/game' + +// Tests unitaires du service game : on mocke fetch, aucune vraie requête HTTP. +const mockFetch = vi.fn() + +function jsonResponse(body, { ok = true, status = 200 } = {}) { + return Promise.resolve({ ok, status, json: () => Promise.resolve(body) }) +} + +beforeEach(() => { + vi.stubGlobal('fetch', mockFetch) + mockFetch.mockReset() + localStorage.setItem('token', 'mon-jwt') +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('myGainsApi', () => { + it('appelle /api/game/my-gains avec le token et renvoie les gains', async () => { + const payload = { gains: [{ id: 1, name: 'Thé détox' }] } + mockFetch.mockReturnValue(jsonResponse(payload)) + + const data = await myGainsApi() + + expect(data).toEqual(payload) + expect(mockFetch).toHaveBeenCalledWith('/api/game/my-gains', { + headers: { Authorization: 'Bearer mon-jwt' }, + }) + }) + + it("lève une erreur avec le status en cas d'échec (ex: session expirée)", async () => { + mockFetch.mockReturnValue( + jsonResponse({ message: 'Non autorisé.' }, { ok: false, status: 401 }) + ) + + await expect(myGainsApi()).rejects.toMatchObject({ message: 'Non autorisé.', status: 401 }) + }) +}) + +describe('participateApi', () => { + it('envoie le code en POST avec le token', async () => { + const payload = { gain: { name: 'Thé signature' }, claim: { id: 1 } } + mockFetch.mockReturnValue(jsonResponse(payload)) + + const data = await participateApi('ABCDE12345') + + expect(data).toEqual(payload) + expect(mockFetch).toHaveBeenCalledWith('/api/game/participate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer mon-jwt', + }, + body: JSON.stringify({ code: 'ABCDE12345' }), + }) + }) + + it("transmet le message d'erreur de l'API (code déjà utilisé, invalide...)", async () => { + mockFetch.mockReturnValue( + jsonResponse({ message: 'Ce code a déjà été utilisé.' }, { ok: false, status: 400 }) + ) + + await expect(participateApi('ABCDE12345')).rejects.toThrow('Ce code a déjà été utilisé.') + }) +}) diff --git a/frontend/src/test/unit/useHandleAuthError.test.jsx b/frontend/src/test/unit/useHandleAuthError.test.jsx new file mode 100644 index 0000000..cd240dc --- /dev/null +++ b/frontend/src/test/unit/useHandleAuthError.test.jsx @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest' +import { useState } from 'react' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useHandleAuthError } from '../../hooks/useHandleAuthError' +import { renderWithProviders } from '../renderWithProviders' + +// Composant de test : déclenche le hook avec l'erreur passée en prop. +function TestComponent({ error }) { + const handleAuthError = useHandleAuthError() + const [handled, setHandled] = useState(null) + return ( +
+ + {handled !== null &&

géré : {String(handled)}

} +
+ ) +} + +describe('useHandleAuthError', () => { + it('déconnecte et redirige vers /connexion sur une erreur 401', async () => { + const error = Object.assign(new Error('Session expirée'), { status: 401 }) + + renderWithProviders(, { + route: '/mes-gains', + user: { email: 'a@b.fr', role: 'CLIENT' }, + routes: { '/connexion':

Page Connexion

}, + }) + + await userEvent.click(screen.getByRole('button', { name: 'déclencher' })) + + expect(screen.getByText('Page Connexion')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBeNull() + expect(localStorage.getItem('user')).toBeNull() + }) + + it('ne fait rien et renvoie false pour une autre erreur', async () => { + const error = Object.assign(new Error('Erreur serveur'), { status: 500 }) + + renderWithProviders(, { + route: '/mes-gains', + user: { email: 'a@b.fr', role: 'CLIENT' }, + routes: { '/connexion':

Page Connexion

}, + }) + + await userEvent.click(screen.getByRole('button', { name: 'déclencher' })) + + expect(screen.getByText('géré : false')).toBeInTheDocument() + expect(localStorage.getItem('token')).toBe('fake-token') + }) +}) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 619db35..8ba6a01 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -9,4 +9,9 @@ export default defineConfig({ '/api': 'http://localhost:5000', }, }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: './src/setupTests.js', + }, })