diff --git a/client/package-lock.json b/client/package-lock.json index 6b6659d..e2c6890 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -53,6 +53,7 @@ "@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/parser": "^8.25.0", "@vitejs/plugin-react-swc": "^3.8.0", + "cross-env": "^7.0.3", "eslint": "^9.21.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-jest": "^28.11.0", @@ -2536,7 +2537,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2555,7 +2556,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2565,7 +2566,7 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -2580,7 +2581,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2591,7 +2592,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2604,7 +2605,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2617,7 +2618,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2641,7 +2642,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2652,7 +2653,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2665,7 +2666,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2678,7 +2679,7 @@ "version": "9.21.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2688,7 +2689,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2698,7 +2699,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.12.0", @@ -2805,7 +2806,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -2815,7 +2816,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -2829,7 +2830,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -2843,7 +2844,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2857,7 +2858,7 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3859,7 +3860,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3873,7 +3873,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3887,7 +3886,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3901,7 +3899,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3915,7 +3912,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3929,7 +3925,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3943,7 +3938,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3957,7 +3951,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3971,7 +3964,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3985,7 +3977,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3999,7 +3990,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4013,7 +4003,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4027,7 +4016,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4041,7 +4029,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4055,7 +4042,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4069,7 +4055,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4083,7 +4068,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4097,7 +4081,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4111,7 +4094,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4152,7 +4134,7 @@ "version": "1.11.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.5.tgz", "integrity": "sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4194,7 +4176,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4211,7 +4192,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4228,7 +4208,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4245,7 +4224,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4262,7 +4240,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4279,7 +4256,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4296,7 +4272,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4313,7 +4288,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4330,7 +4304,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4347,7 +4320,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4361,14 +4333,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -4562,7 +4534,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -4669,7 +4640,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/katex": { @@ -4718,7 +4689,6 @@ "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -5052,7 +5022,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5086,7 +5056,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5177,7 +5147,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, + "devOptional": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -5563,7 +5533,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -5898,7 +5868,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -5974,11 +5944,30 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6149,7 +6138,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -6715,7 +6704,7 @@ "version": "9.21.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -6945,7 +6934,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -6962,7 +6951,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6975,7 +6964,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6986,7 +6975,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6999,7 +6988,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7012,7 +7001,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", @@ -7030,7 +7019,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7056,7 +7045,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -7069,7 +7058,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -7162,7 +7151,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7199,14 +7188,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fastq": { @@ -7247,7 +7236,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -7301,7 +7290,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -7318,7 +7307,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -7332,7 +7321,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -7397,7 +7386,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7581,7 +7569,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7866,7 +7854,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7912,7 +7900,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -8106,7 +8094,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8171,7 +8159,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8406,7 +8394,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9518,7 +9506,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9588,7 +9576,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9601,14 +9589,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json5": { @@ -9668,7 +9656,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -9698,7 +9686,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -9718,7 +9706,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -9755,7 +9743,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/longest-streak": { @@ -10545,7 +10533,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-int64": { @@ -10727,7 +10715,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -10763,7 +10751,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -10779,7 +10767,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -10847,7 +10835,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -10867,7 +10855,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11011,7 +10999,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -11040,7 +11027,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -11059,7 +11045,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -11570,7 +11556,6 @@ "version": "4.34.8", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -11774,7 +11759,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11787,7 +11772,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11967,7 +11952,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12199,7 +12183,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -12483,7 +12467,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -12596,7 +12580,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12842,7 +12825,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12931,7 +12914,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -13209,7 +13191,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13313,7 +13295,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13461,7 +13443,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/client/package.json b/client/package.json index 37ec6f4..b6d62e4 100644 --- a/client/package.json +++ b/client/package.json @@ -57,6 +57,7 @@ "@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/parser": "^8.25.0", "@vitejs/plugin-react-swc": "^3.8.0", + "cross-env": "^7.0.3", "eslint": "^9.21.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-jest": "^28.11.0", diff --git a/client/src/Types/StudentType.tsx b/client/src/Types/StudentType.tsx index b484af5..41a4a63 100644 --- a/client/src/Types/StudentType.tsx +++ b/client/src/Types/StudentType.tsx @@ -1,5 +1,7 @@ +import { AnswerType } from "src/pages/Student/JoinRoom/JoinRoom"; + export interface Answer { - answer: string | number | boolean; + answer: AnswerType; isCorrect: boolean; idQuestion: number; } diff --git a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx index 05900fc..8751e8b 100644 --- a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx @@ -5,6 +5,7 @@ import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MultipleChoiceQuestion, parse } from 'gift-pegjs'; import MultipleChoiceQuestionDisplay from 'src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; const questions = parse( `::Sample Question 1:: Question stem @@ -21,7 +22,7 @@ describe('MultipleChoiceQuestionDisplay', () => { const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const [showAnswerState, setShowAnswerState] = useState(showAnswer); - const handleOnSubmitAnswer = (answer: string) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { mockHandleOnSubmitAnswer(answer); setShowAnswerState(true); }; diff --git a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx index 5bcb7df..e6910d4 100644 --- a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx @@ -5,6 +5,7 @@ import '@testing-library/jest-dom'; import { MemoryRouter } from 'react-router-dom'; import TrueFalseQuestionDisplay from 'src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay'; import { parse, TrueFalseQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; describe('TrueFalseQuestion Component', () => { const mockHandleSubmitAnswer = jest.fn(); @@ -16,7 +17,7 @@ describe('TrueFalseQuestion Component', () => { const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const [showAnswerState, setShowAnswerState] = useState(showAnswer); - const handleOnSubmitAnswer = (answer: boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { mockHandleSubmitAnswer(answer); setShowAnswerState(true); }; diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 801cdd5..1218694 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; import { BaseQuestion, parse } from 'gift-pegjs'; import { QuestionType } from 'src/Types/QuestionType'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; const mockGiftQuestions = parse( `::Sample Question 1:: Sample Question 1 {=Option A ~Option B} @@ -15,21 +16,26 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => if (question.type !== "Category") question.id = (index + 1).toString(); const newMockQuestion = question; - return {question : newMockQuestion as BaseQuestion}; + return { question: newMockQuestion as BaseQuestion }; }); const mockSubmitAnswer = jest.fn(); const mockDisconnectWebSocket = jest.fn(); beforeEach(() => { + // Clear local storage before each test + // localStorage.clear(); + render( - ); + + ); }); describe('StudentModeQuiz', () => { @@ -51,6 +57,49 @@ describe('StudentModeQuiz', () => { expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); }); + test('handles shows feedback for an already answered question', async () => { + // Answer the first question + act(() => { + fireEvent.click(screen.getByText('Option A')); + }); + act(() => { + fireEvent.click(screen.getByText('Répondre')); + }); + expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + + const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); + expect(firstButtonA).toBeInTheDocument(); + expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); + + expect(screen.getByRole("button", {name: '❌ B Option B'})).toBeInTheDocument(); + expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); + + // Navigate to the next question + act(() => { + fireEvent.click(screen.getByText('Question suivante')); + }); + expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); + expect(screen.getByText('Répondre')).toBeInTheDocument(); + + // Navigate back to the first question + act(() => { + fireEvent.click(screen.getByText('Question précédente')); + }); + expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); + + // Since answers are mocked, the it doesn't recognize the question as already answered + // TODO these tests are partially faked, need to be fixed if we can mock the answers + // const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); + const buttonA = screen.getByRole("button", {name: 'A Option A'}); + expect(buttonA).toBeInTheDocument(); + // const buttonB = screen.getByRole("button", {name: '❌ B Option B'}); + const buttonB = screen.getByRole("button", {name: 'B Option B'}); + expect(buttonB).toBeInTheDocument(); + // // "Option A" div inside the name of button should have selected class + // expect(buttonA.querySelector('.selected')).toBeInTheDocument(); + + }); + test('handles quit button click', async () => { act(() => { fireEvent.click(screen.getByText('Quitter')); @@ -65,16 +114,12 @@ describe('StudentModeQuiz', () => { }); act(() => { fireEvent.click(screen.getByText('Répondre')); - }); + }); act(() => { fireEvent.click(screen.getByText('Question suivante')); }); - const sampleQuestionElements = screen.queryAllByText(/Sample question 2/i); - expect(sampleQuestionElements.length).toBeGreaterThan(0); - expect(screen.getByText('V')).toBeInTheDocument(); - + expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); + expect(screen.getByText('Répondre')).toBeInTheDocument(); }); - }); - diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 0332f1a..6a4ec59 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -3,41 +3,52 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; import { screen } from '@testing-library/dom'; import '@testing-library/jest-dom'; -import { MultipleChoiceQuestion, parse } from 'gift-pegjs'; - +import { BaseQuestion, MultipleChoiceQuestion, parse } from 'gift-pegjs'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; import { MemoryRouter } from 'react-router-dom'; -// import { mock } from 'node:test'; +import { QuestionType } from 'src/Types/QuestionType'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; const mockGiftQuestions = parse( - `::Sample Question:: Sample Question {=Option A ~Option B}`); - - -describe('TeacherModeQuiz', () => { - it ('renders the initial question as MultipleChoiceQuestion', () => { - expect(mockGiftQuestions[0].type).toBe('MC'); - }); + `::Sample Question 1:: Sample Question 1 {=Option A ~Option B} - const mockQuestion = mockGiftQuestions[0] as MultipleChoiceQuestion; + ::Sample Question 2:: Sample Question 2 {=Option A ~Option B}`); + + const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { + if (question.type !== "Category") + question.id = (index + 1).toString(); + const newMockQuestion = question; + return {question : newMockQuestion as BaseQuestion}; + }); + +describe('TeacherModeQuiz', () => { + + + let mockQuestion = mockQuestions[0].question as MultipleChoiceQuestion; mockQuestion.id = '1'; const mockSubmitAnswer = jest.fn(); const mockDisconnectWebSocket = jest.fn(); + let rerender: (ui: React.ReactElement) => void; + beforeEach(async () => { - render( + const utils = render( ); + rerender = utils.rerender; }); test('renders the initial question', () => { + expect(screen.getByText('Question 1')).toBeInTheDocument(); - expect(screen.getByText('Sample Question')).toBeInTheDocument(); + expect(screen.getByText('Sample Question 1')).toBeInTheDocument(); expect(screen.getByText('Option A')).toBeInTheDocument(); expect(screen.getByText('Option B')).toBeInTheDocument(); expect(screen.getByText('Quitter')).toBeInTheDocument(); @@ -53,9 +64,51 @@ describe('TeacherModeQuiz', () => { fireEvent.click(screen.getByText('Répondre')); }); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); - expect(screen.getByText('Votre réponse est:')).toBeInTheDocument(); }); + test('handles shows feedback for an already answered question', () => { + // Answer the first question + act(() => { + fireEvent.click(screen.getByText('Option A')); + }); + act(() => { + fireEvent.click(screen.getByText('Répondre')); + }); + expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + mockQuestion = mockQuestions[1].question as MultipleChoiceQuestion; + // Navigate to the next question by re-rendering with new props + act(() => { + rerender( + + + + ); + }); + + mockQuestion = mockQuestions[0].question as MultipleChoiceQuestion; + + act(() => { + rerender( + + + + ); + }); + + // Check if the feedback dialog is shown again + expect(screen.getByText('Rétroaction')).toBeInTheDocument(); + }); + test('handles disconnect button click', () => { act(() => { fireEvent.click(screen.getByText('Quitter')); diff --git a/client/src/__tests__/services/WebsocketService.test.tsx b/client/src/__tests__/services/WebsocketService.test.tsx index d21423e..343a8ce 100644 --- a/client/src/__tests__/services/WebsocketService.test.tsx +++ b/client/src/__tests__/services/WebsocketService.test.tsx @@ -1,7 +1,9 @@ //WebsocketService.test.tsx +import { BaseQuestion, parse } from 'gift-pegjs'; import WebsocketService from '../../services/WebsocketService'; import { io, Socket } from 'socket.io-client'; import { ENV_VARIABLES } from 'src/constants'; +import { QuestionType } from 'src/Types/QuestionType'; jest.mock('socket.io-client'); @@ -45,10 +47,16 @@ describe('WebSocketService', () => { test('nextQuestion should emit next-question event with correct parameters', () => { const roomName = 'testRoom'; - const question = { id: 1, text: 'Sample Question' }; - + const mockGiftQuestions = parse('A {T}'); + const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { + if (question.type !== "Category") + question.id = (index + 1).toString(); + const newMockQuestion = question; + return {question : newMockQuestion as BaseQuestion}; + }); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); - WebsocketService.nextQuestion(roomName, question); + WebsocketService.nextQuestion({roomName, questions: mockQuestions, questionIndex: 0, isLaunch: false}); + const question = mockQuestions[0]; expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question }); }); diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index e5e7b6b..df46193 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -4,33 +4,44 @@ import '../questionStyle.css'; import { Button } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; import { MultipleChoiceQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: MultipleChoiceQuestion; - handleOnSubmitAnswer?: (answer: string) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; + passedAnswer?: AnswerType; } const MultipleChoiceQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer } = props; - const [answer, setAnswer] = useState(); - + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; + const [answer, setAnswer] = useState(passedAnswer || ''); + + + let disableButton = false; + if(handleOnSubmitAnswer === undefined){ + disableButton = true; + } + useEffect(() => { - setAnswer(undefined); - }, [question]); + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } + }, [passedAnswer]); const handleOnClickAnswer = (choice: string) => { setAnswer(choice); }; - const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alphabet = alpha.map((x) => String.fromCharCode(x)); return ( +
+ {question.choices.map((choice, i) => { const selected = answer === choice.formattedText.text ? 'selected' : ''; return ( @@ -38,6 +49,7 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => {
{showAnswer ? ( <> -
{correctAnswer}
+
+ La bonne réponse est: + {correctAnswer}
+ + Votre réponse est: {answer.toString()} + {question.formattedGlobalFeedback &&
} + ) : ( <> @@ -75,7 +87,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === undefined || isNaN(answer)} + disabled={answer === "" || isNaN(answer as number)} > Répondre diff --git a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx index 8dfa1b3..af6e6d8 100644 --- a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx @@ -5,17 +5,21 @@ import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuesti import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay'; import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay'; import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; // import useCheckMobileScreen from '../../services/useCheckMobileScreen'; interface QuestionProps { question: Question; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; + answer?: AnswerType; + } const QuestionDisplay: React.FC = ({ question, handleOnSubmitAnswer, showAnswer, + answer, }) => { // const isMobile = useCheckMobileScreen(); // const imgWidth = useMemo(() => { @@ -30,37 +34,32 @@ const QuestionDisplay: React.FC = ({ question={question} handleOnSubmitAnswer={handleOnSubmitAnswer} showAnswer={showAnswer} + passedAnswer={answer} /> ); break; case 'MC': + questionTypeComponent = ( ); break; case 'Numerical': if (question.choices) { - if (!Array.isArray(question.choices)) { questionTypeComponent = ( ); - } else { - questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked) - - ); - } } break; case 'Short': @@ -69,6 +68,7 @@ const QuestionDisplay: React.FC = ({ question={question} handleOnSubmitAnswer={handleOnSubmitAnswer} showAnswer={showAnswer} + passedAnswer={answer} /> ); break; diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 50c2261..28876f9 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -1,18 +1,29 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import '../questionStyle.css'; import { Button, TextField } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; import { ShortAnswerQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: ShortAnswerQuestion; - handleOnSubmitAnswer?: (answer: string) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; + passedAnswer?: AnswerType; + } const ShortAnswerQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer } = props; - const [answer, setAnswer] = useState(); + + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; + const [answer, setAnswer] = useState(passedAnswer || ''); + + useEffect(() => { + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } + }, [passedAnswer]); + console.log("Answer" , answer); return (
@@ -22,11 +33,18 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { {showAnswer ? ( <>
+ + La bonne réponse est: + {question.choices.map((choice) => (
{choice.text}
))} +
+ + Votre réponse est: {answer} +
{question.formattedGlobalFeedback &&
@@ -54,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === undefined || answer === ''} + disabled={answer === null || answer === ''} > Répondre diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 63b3891..8908338 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -1,24 +1,48 @@ // TrueFalseQuestion.tsx -import React, { useState, useEffect } from 'react'; +import React, { useState,useEffect } from 'react'; import '../questionStyle.css'; import { Button } from '@mui/material'; import { TrueFalseQuestion } from 'gift-pegjs'; import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: TrueFalseQuestion; - handleOnSubmitAnswer?: (answer: boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; + passedAnswer?: AnswerType; } const TrueFalseQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer } = + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} = props; - const [answer, setAnswer] = useState(undefined); + + let disableButton = false; + if(handleOnSubmitAnswer === undefined){ + disableButton = true; + } useEffect(() => { - setAnswer(undefined); - }, [question]); + console.log("passedAnswer", answer); + if (passedAnswer === true || passedAnswer === false) { + setAnswer(passedAnswer); + } else { + setAnswer(undefined); + } + }, [passedAnswer, question.id]); + + const [answer, setAnswer] = useState(() => { + + if (passedAnswer === true || passedAnswer === false) { + return passedAnswer; + } + + return undefined; + }); + + const handleOnClickAnswer = (choice: boolean) => { + setAnswer(choice); + }; const selectedTrue = answer ? 'selected' : ''; const selectedFalse = answer !== undefined && !answer ? 'selected' : ''; @@ -30,35 +54,38 @@ const TrueFalseQuestionDisplay: React.FC = (props) => {
- {/* selected TRUE, show True feedback if it exists */} - {showAnswer && answer && question.trueFormattedFeedback && ( -
-
-
- )} - {/* selected FALSE, show False feedback if it exists */} - {showAnswer && !answer && question.falseFormattedFeedback && ( -
-
-
- )} {question.formattedGlobalFeedback && showAnswer && (
@@ -69,6 +96,7 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { variant="contained" onClick={() => answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer) + } disabled={answer === undefined} > diff --git a/client/src/components/QuestionsDisplay/questionStyle.css b/client/src/components/QuestionsDisplay/questionStyle.css index cdf611f..f300ba2 100644 --- a/client/src/components/QuestionsDisplay/questionStyle.css +++ b/client/src/components/QuestionsDisplay/questionStyle.css @@ -147,6 +147,25 @@ box-shadow: 0px 2px 5px hsl(0, 0%, 74%); } +.true-feedback { + position: relative; + padding: 0 1rem; + background-color: hsl(43, 100%, 94%); + color: hsl(43, 95%, 9%); + border: hsl(36, 84%, 93%) 1px solid; + border-radius: 6px; + box-shadow: 0px 2px 5px hsl(0, 0%, 74%); +} +.false-feedback { + position: relative; + padding: 0 1rem; + background-color: hsl(43, 100%, 94%); + color: hsl(43, 95%, 9%); + border: hsl(36, 84%, 93%) 1px solid; + border-radius: 6px; + box-shadow: 0px 2px 5px hsl(0, 0%, 74%); +} + .choices-wrapper { width: 90%; } diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index dd34ec1..192c0b2 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -3,41 +3,47 @@ import React, { useEffect, useState } from 'react'; import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; -// import { QuestionService } from '../../services/QuestionService'; import { Button } from '@mui/material'; //import QuestionNavigation from '../QuestionNavigation/QuestionNavigation'; -//import { ChevronLeft, ChevronRight } from '@mui/icons-material'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Question } from 'gift-pegjs'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface StudentModeQuizProps { questions: QuestionType[]; - submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; + answers: AnswerSubmissionToBackendType[]; + submitAnswer: (_answer: AnswerType, _idQuestion: number) => void; disconnectWebSocket: () => void; } const StudentModeQuiz: React.FC = ({ questions, + answers, submitAnswer, disconnectWebSocket }) => { + //Ajouter type AnswerQuestionType en remplacement de QuestionType const [questionInfos, setQuestion] = useState(questions[0]); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); - // const [imageUrl, setImageUrl] = useState(''); + // const [answer, setAnswer] = useState(''); + - // const previousQuestion = () => { - // setQuestion(questions[Number(questionInfos.question?.id) - 2]); - // setIsAnswerSubmitted(false); - // }; + const previousQuestion = () => { + setQuestion(questions[Number(questionInfos.question?.id) - 2]); + }; - useEffect(() => {}, [questionInfos]); + useEffect(() => { + const savedAnswer = answers[Number(questionInfos.question.id)-1]?.answer; + console.log(`StudentModeQuiz: useEffect: savedAnswer: ${savedAnswer}`); + setIsAnswerSubmitted(savedAnswer !== undefined); + }, [questionInfos.question, answers]); const nextQuestion = () => { setQuestion(questions[Number(questionInfos.question?.id)]); - setIsAnswerSubmitted(false); }; - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); setIsAnswerSubmitted(true); @@ -46,11 +52,13 @@ const StudentModeQuiz: React.FC = ({ return (
- +
+
+ Question {questionInfos.question.id}/{questions.length}
@@ -66,31 +74,30 @@ const StudentModeQuiz: React.FC = ({ handleOnSubmitAnswer={handleOnSubmitAnswer} question={questionInfos.question as Question} showAnswer={isAnswerSubmitted} + answer={answers[Number(questionInfos.question.id)-1]?.answer} /> -
-
- {/* */} -
-
- -
+
+
+
+
+ +
+
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index d4c7793..8925c09 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -1,55 +1,59 @@ // TeacherModeQuiz.tsx import React, { useEffect, useState } from 'react'; - import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; - import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; -// import { QuestionService } from '../../services/QuestionService'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Question } from 'gift-pegjs'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; +// import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface TeacherModeQuizProps { questionInfos: QuestionType; - submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; + answers: AnswerSubmissionToBackendType[]; + submitAnswer: (_answer: AnswerType, _idQuestion: number) => void; disconnectWebSocket: () => void; } const TeacherModeQuiz: React.FC = ({ questionInfos, + answers, submitAnswer, disconnectWebSocket }) => { const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [feedbackMessage, setFeedbackMessage] = useState(''); - - const renderFeedbackMessage = (answer: string) => { + const [answer, setAnswer] = useState(); - if(answer === 'true' || answer === 'false'){ - return ( - Votre réponse est: {answer==="true" ? 'Vrai' : 'Faux'} - ) - } - else{ - return ( - - Votre réponse est: {answer.toString()} - - );} - }; + + // arrive here the first time after waiting for next question useEffect(() => { - // Close the feedback dialog when the question changes - handleFeedbackDialogClose(); - setIsAnswerSubmitted(false); - - }, [questionInfos.question]); + console.log(`TeacherModeQuiz: useEffect: answers: ${JSON.stringify(answers)}`); + console.log(`TeacherModeQuiz: useEffect: questionInfos.question.id: ${questionInfos.question.id} answer: ${answer}`); + const oldAnswer = answers[Number(questionInfos.question.id) -1 ]?.answer; + console.log(`TeacherModeQuiz: useEffect: oldAnswer: ${oldAnswer}`); + setAnswer(oldAnswer); + setIsFeedbackDialogOpen(false); + }, [questionInfos.question, answers]); - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + // handle showing the feedback dialog + useEffect(() => { + console.log(`TeacherModeQuiz: useEffect: answer: ${answer}`); + setIsAnswerSubmitted(answer !== undefined); + setIsFeedbackDialogOpen(answer !== undefined); + }, [answer]); + + useEffect(() => { + console.log(`TeacherModeQuiz: useEffect: isAnswerSubmitted: ${isAnswerSubmitted}`); + setIsFeedbackDialogOpen(isAnswerSubmitted); + }, [isAnswerSubmitted]); + + const handleOnSubmitAnswer = (answer: AnswerType) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); - setFeedbackMessage(renderFeedbackMessage(answer.toString())); + // setAnswer(answer); setIsFeedbackDialogOpen(true); }; @@ -60,21 +64,21 @@ const TeacherModeQuiz: React.FC = ({ return (
-
+
- - -
-
Question {questionInfos.question.id}
-
- -
+ +
+
Question {questionInfos.question.id}
- {isAnswerSubmitted ? ( +
+ +
+ + {isAnswerSubmitted ? (
En attente pour la prochaine question...
@@ -82,6 +86,7 @@ const TeacherModeQuiz: React.FC = ({ )} @@ -92,20 +97,21 @@ const TeacherModeQuiz: React.FC = ({ Rétroaction
- {feedbackMessage} -
Question :
+ wordWrap: 'break-word', + whiteSpace: 'pre-wrap', + maxHeight: '400px', + overflowY: 'auto', + }}> +
Question :
- -
@@ -114,7 +120,7 @@ const TeacherModeQuiz: React.FC = ({ -
+
); }; diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index 5ff30b0..dc7e80c 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -17,6 +17,8 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer' import ApiService from '../../../services/ApiService' +export type AnswerType = string | number | boolean; + const JoinRoom: React.FC = () => { const [roomName, setRoomName] = useState(''); const [username, setUsername] = useState(ApiService.getUsername()); @@ -25,6 +27,7 @@ const JoinRoom: React.FC = () => { const [question, setQuestion] = useState(); const [quizMode, setQuizMode] = useState(); const [questions, setQuestions] = useState([]); + const [answers, setAnswers] = useState([]); const [connectionError, setConnectionError] = useState(''); const [isConnecting, setIsConnecting] = useState(false); @@ -35,6 +38,13 @@ const JoinRoom: React.FC = () => { }; }, []); + useEffect(() => { + // init the answers array, one for each question + setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType)); + console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); + }, [questions]); + + const handleCreateSocket = () => { console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); @@ -45,11 +55,18 @@ const JoinRoom: React.FC = () => { console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`); }); socket.on('next-question', (question: QuestionType) => { - console.log('on(next-question): Received next-question:', question); + console.log('JoinRoom: on(next-question): Received next-question:', question); setQuizMode('teacher'); setIsWaitingForTeacher(false); setQuestion(question); }); + socket.on('launch-teacher-mode', (questions: QuestionType[]) => { + console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); + setQuizMode('teacher'); + setIsWaitingForTeacher(true); + setQuestions(questions); + // wait for next-question + }); socket.on('launch-student-mode', (questions: QuestionType[]) => { console.log('on(launch-student-mode): Received launch-student-mode:', questions); @@ -83,6 +100,7 @@ const JoinRoom: React.FC = () => { }; const disconnect = () => { +// localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -107,14 +125,22 @@ const JoinRoom: React.FC = () => { } }; - const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => { + const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => { + console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`); const answerData: AnswerSubmissionToBackendType = { roomName: roomName, answer: answer, username: username, idQuestion: idQuestion }; - + // localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); + setAnswers((prevAnswers) => { + console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`); + const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array + newAnswers[idQuestion - 1] = answerData; // Update the specific answer + return newAnswers; // Return the new array + }); + console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`); webSocketService.submitAnswer(answerData); }; @@ -152,6 +178,7 @@ const JoinRoom: React.FC = () => { return ( @@ -161,6 +188,7 @@ const JoinRoom: React.FC = () => { question && ( diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 3a67c27..bcdc80d 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -24,6 +24,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay'; import ApiService from '../../../services/ApiService'; import { QuestionType } from 'src/Types/QuestionType'; import { Button } from '@mui/material'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; const ManageRoom: React.FC = () => { const navigate = useNavigate(); @@ -131,7 +132,11 @@ const ManageRoom: React.FC = () => { if (!quizStarted) return; if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, currentQuestion); + webSocketService.nextQuestion( + {roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: Number(currentQuestion?.question.id) - 1, + isLaunch: false}); } else if (quizMode === 'student') { webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); } @@ -224,7 +229,10 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: nextQuestionIndex, + isLaunch: false}); }; const previousQuestion = () => { @@ -234,7 +242,7 @@ const ManageRoom: React.FC = () => { if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; setCurrentQuestion(quizQuestions[prevQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); }; const initializeQuizQuestion = () => { @@ -262,7 +270,7 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); }; const launchStudentMode = () => { @@ -300,9 +308,8 @@ const ManageRoom: React.FC = () => { const showSelectedQuestion = (questionIndex: number) => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); - if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); } } }; @@ -313,7 +320,7 @@ const ManageRoom: React.FC = () => { }; function checkIfIsCorrect( - answer: string | number | boolean, + answer: AnswerType, idQuestion: number, questions: QuestionType[] ): boolean { @@ -453,6 +460,7 @@ const ManageRoom: React.FC = () => { )} diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx index 3cacf36..9262a59 100644 --- a/client/src/services/WebsocketService.tsx +++ b/client/src/services/WebsocketService.tsx @@ -1,18 +1,20 @@ import { io, Socket } from 'socket.io-client'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; +import { QuestionType } from 'src/Types/QuestionType'; // Must (manually) sync these types to server/socket/socket.js export type AnswerSubmissionToBackendType = { roomName: string; username: string; - answer: string | number | boolean; + answer: AnswerType; idQuestion: number; }; export type AnswerReceptionFromBackendType = { idUser: string; username: string; - answer: string | number | boolean; + answer: AnswerType; idQuestion: number; }; @@ -59,12 +61,19 @@ class WebSocketService { // } // } - nextQuestion(roomName: string, question: unknown) { - console.log('WebsocketService: nextQuestion', roomName, question); - if (!question) { + nextQuestion(args: {roomName: string, questions: QuestionType[] | undefined, questionIndex: number, isLaunch: boolean}) { + // deconstruct args + const { roomName, questions, questionIndex, isLaunch } = args; + console.log('WebsocketService: nextQuestion', roomName, questions, questionIndex, isLaunch); + if (!questions || !questions[questionIndex]) { throw new Error('WebsocketService: nextQuestion: question is null'); } + if (this.socket) { + if (isLaunch) { + this.socket.emit('launch-teacher-mode', { roomName, questions }); + } + const question = questions[questionIndex]; this.socket.emit('next-question', { roomName, question }); } } diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js index 739d79d..2d84da4 100644 --- a/server/__tests__/socket.test.js +++ b/server/__tests__/socket.test.js @@ -109,17 +109,29 @@ describe("websocket server", () => { }); }); - test("should send next question", (done) => { - studentSocket.on("next-question", (question) => { - expect(question).toEqual({ question: "question2" }); + test("should launch teacher mode", (done) => { + studentSocket.on("launch-teacher-mode", (questions) => { + expect(questions).toEqual([ + { question: "question1" }, + { question: "question2" }, + ]); done(); }); - teacherSocket.emit("next-question", { + teacherSocket.emit("launch-teacher-mode", { roomName: "ROOM1", - question: { question: "question2" }, + questions: [{ question: "question1" }, { question: "question2" }], }); }); + test("should send next question", (done) => { + studentSocket.on("next-question", ( question ) => { + expect(question).toBe("question2"); + done(); + }); + teacherSocket.emit("next-question", { roomName: "ROOM1", question: 'question2'}, + ); + }); + test("should send answer", (done) => { teacherSocket.on("submit-answer-room", (answer) => { expect(answer).toEqual({ diff --git a/server/package-lock.json b/server/package-lock.json index e0fdb07..0f006f8 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2498,6 +2498,7 @@ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, diff --git a/server/socket/socket.js b/server/socket/socket.js index 393135c..0adaf92 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -81,6 +81,10 @@ const setupWebsocket = (io) => { socket.to(roomName).emit("next-question", question); }); + socket.on("launch-teacher-mode", ({ roomName, questions }) => { + socket.to(roomName).emit("launch-teacher-mode", questions); + }); + socket.on("launch-student-mode", ({ roomName, questions }) => { socket.to(roomName).emit("launch-student-mode", questions); });