Compare commits

...

94 commits

Author SHA1 Message Date
Christopher (Cris) Fuhrman
6f270b5436
Merge pull request #198 from ets-cfuhrman-pfe/JubaAzul/issue197
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Juba azul/issue197
2025-01-22 20:55:08 -05:00
Christopher (Cris) Fuhrman
4b133400d4
Merge pull request #205 from ets-cfuhrman-pfe/JubaAzul/issue145
Juba azul/issue145
2025-01-22 20:49:54 -05:00
Christopher (Cris) Fuhrman
70e74e2342
Merge pull request #206 from ets-cfuhrman-pfe/JubaAzul/issue74
Update editorQuiz.css
2025-01-22 20:48:13 -05:00
JubaAzul
da4e54534e Update editorQuiz.css 2025-01-22 16:46:09 -05:00
JubaAzul
5018625693 Fix tests 2025-01-22 16:06:23 -05:00
JubaAzul
22f988f2ad Confusion avec la navigation dans les questions à rythme de l'enseignant
Fixes #145
2025-01-22 15:28:45 -05:00
JubaAzul
3e9152fa5c Confusion avec la navigation dans les questions à rythme de l'enseignant
Fixes #145
2025-01-21 15:35:07 -05:00
JubaAzul
394bcf8bc4 enlever modifications de eslint 2025-01-20 12:23:02 -05:00
JubaAzul
70bad72350 Update eslint.config.js 2025-01-20 12:17:50 -05:00
JubaAzul
0a6383b2ec Exemples (copiés) ne sont pas valides et les directives ne sont pas bonnes
Fixes #197
2025-01-20 12:16:21 -05:00
C. Fuhrman
a44cded030 Ajouter watchtower_once -- chercher les mises à jour immédiatement
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
2025-01-17 23:00:48 -05:00
Christopher (Cris) Fuhrman
df061fae1a
Merge pull request #183 from ets-cfuhrman-pfe/add-composant-and-snapshot-tests-for-the-EditorQuiz
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Add composant and snapshot tests for the editor quiz
2025-01-17 18:58:55 -05:00
JubaAzul
a04ec3a2e7
Merge branch 'main' into add-composant-and-snapshot-tests-for-the-EditorQuiz 2025-01-17 10:50:04 -05:00
Christopher (Cris) Fuhrman
4f58c4d412
Merge pull request #194 from ets-cfuhrman-pfe/Scroll-up-arrow-inside-quizEditor
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Button pour monter en haut de la page d'éditeur de quiz
2025-01-16 17:05:41 -05:00
Christopher (Cris) Fuhrman
b9eb2201d7
Merge pull request #195 from ets-cfuhrman-pfe/JubaAzul/issue192
Risques sécurité dangerouslySetInnerHTML()
2025-01-16 17:00:00 -05:00
JubaAzul
734f48b30a Ajout de configuration de jest.config.cjs 2025-01-16 16:22:16 -05:00
JubaAzul
f997fea3c3 Enlever erreur dans .yml 2025-01-16 12:40:04 -05:00
JubaAzul
1c928c8350 Chemin absolu pour les imports 2025-01-16 12:37:07 -05:00
JubaAzul
89dd55ca8f try import react inside class 2025-01-16 12:02:13 -05:00
JubaAzul
b46e2e3985 removed import in every class, try to update react 2025-01-16 12:02:13 -05:00
JubaAzul
6d56454b02 remove change in .eslintrc.cjs, workflow did not run 2025-01-16 12:02:07 -05:00
JubaAzul
0a932d9617 ignore @ts-expect-error in pipeline 2025-01-16 12:02:07 -05:00
JubaAzul
3eb7ef084c Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer 2025-01-16 12:00:48 -05:00
JubaAzul
3c9b14dd9d Used "@ts-expect-error" instead of "@ts-ignore" 2025-01-16 12:00:48 -05:00
JubaAzul
15805e2e7e import react inside class for pipeline, not IDE 2025-01-16 11:59:02 -05:00
C. Fuhrman
66d57fd451 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
2025-01-16 11:20:44 -05:00
JubaAzul
159ed5fa5c removed import in every class, try to update react 2025-01-16 10:40:27 -05:00
JubaAzul
bf97931995 Run with change ob frontend.yml 2025-01-15 13:20:50 -05:00
JubaAzul
f595ccca20 remove change in .eslintrc.cjs, workflow did not run 2025-01-15 11:47:52 -05:00
JubaAzul
d6774b300f ignore @ts-expect-error in pipeline 2025-01-15 11:08:17 -05:00
JubaAzul
470d856afc Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer 2025-01-15 10:40:40 -05:00
JubaAzul
8d14e49095 Used "@ts-expect-error" instead of "@ts-ignore" 2025-01-15 10:30:45 -05:00
JubaAzul
9444603a93 import react inside class for pipeline, not IDE 2025-01-15 10:27:28 -05:00
JubaAzul
77d5ae2862 Fix pipeline, error tests 2025-01-15 09:29:46 -05:00
JubaAzul
f43da4c8ba Risques sécurité dangerouslySetInnerHTML()
Fixes #192
2025-01-15 09:07:56 -05:00
JubaAzul
d18c2c502a Button pour monter en haut de la page d'éditeur de quiz 2025-01-14 17:37:29 -05:00
C. Fuhrman
86b127378b Script to backup the mongo db 2025-01-14 11:18:52 -05:00
JubaAzul
59b7fa02e0 try import react inside class 2025-01-13 20:03:12 -05:00
JubaAzul
b834c8f06f Import react in tests classes 2025-01-13 19:47:37 -05:00
JubaAzul
f201305064 Revert node change 2025-01-13 18:47:28 -05:00
JubaAzul
12b376d606 Correciton pull 2025-01-13 18:34:25 -05:00
JubaAzul
6e39587c68 Update node 2025-01-13 10:25:09 -05:00
Christopher (Cris) Fuhrman
a7e65d4095
Merge pull request #189 from ets-cfuhrman-pfe/valkyrie1-patch-1
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Update LICENSE
2025-01-12 22:40:04 -05:00
valkyrie1
c290ba0080
Update LICENSE
ajout des noms pour hiver 2024
2025-01-12 22:15:09 -05:00
Christopher (Cris) Fuhrman
4c1f2b4501
Merge pull request #188 from ets-cfuhrman-pfe/fuhrmanator/issue187
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Corriger la config des variables d'environnement development/production (docker)
2025-01-11 11:33:40 -05:00
C. Fuhrman
afef820765 isDev n'était pas passé en paramètre (undefined) 2025-01-11 11:30:01 -05:00
C. Fuhrman
7c5c6739fa Définir les variables VITE_* (vide)
Vite est censé ne pas les changer par un .env.* (selon copilot)
2025-01-11 11:28:51 -05:00
C. Fuhrman
ec2f6cc358 Renommer .env à .env.development
Vite est censé ignorer ça en production (docker) tandis que .env serait toujours chargé
2025-01-11 11:27:36 -05:00
Christopher (Cris) Fuhrman
da3e810a32
Merge pull request #186 from ets-cfuhrman-pfe/fuhrmanator/issue185
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Fuhrmanator/issue185
2025-01-11 03:03:18 -05:00
C. Fuhrman
3c9a11e668 tweak eslint command 2025-01-11 03:01:40 -05:00
C. Fuhrman
236aaa6d09 more feedback on action 2025-01-11 02:47:48 -05:00
C. Fuhrman
63a3131729 run lint 2025-01-11 02:41:26 -05:00
C. Fuhrman
9fa277c8d7 fix errors: 'React' is declared but its value is never read. 2025-01-11 02:32:14 -05:00
C. Fuhrman
50e92fb458 install eslint (client), fix errors (many) 2025-01-11 02:22:14 -05:00
C. Fuhrman
fd81e3b0d1 Install eslint in server, fix errors
configure
2025-01-11 00:03:05 -05:00
Christopher (Cris) Fuhrman
e5e7f61b71
Merge pull request #184 from ets-cfuhrman-pfe/fuhrmanator/issue171
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Fuhrmanator/issue171
2025-01-10 23:46:01 -05:00
C. Fuhrman
90e42865ba Clean up server startup
Fixes #171
2025-01-10 23:43:15 -05:00
Christopher (Cris) Fuhrman
839ee79912
Merge pull request #153 from ets-cfuhrman-pfe/fuhrmanator/issue78
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Nom du dossier pour chaque quiz devrait être affiché lorsque "tous les dossiers" sont affichés
2025-01-10 22:38:07 -05:00
Christopher (Cris) Fuhrman
2f14ac0ad2
Merge pull request #182 from ets-cfuhrman-pfe/JubaAzul/issue181
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Impossible de téleverser une image (Erreur 505)
2025-01-10 18:06:08 -05:00
JubaAzul
427182e7a8 Adding snapshot tests for editor 2025-01-10 17:39:19 -05:00
JubaAzul
ee00feef69 Impossible de téleverser une image (Erreur 505)
Fixes #181
2025-01-10 16:27:00 -05:00
C. Fuhrman
ea6454550c vite needs to understand the src mapping (redundant with tsconfig.json) 2025-01-10 15:56:43 -05:00
C. Fuhrman
6c73cfddc9 use src/constants (abs path) so it can remap when jest runs 2025-01-10 15:46:17 -05:00
C. Fuhrman
51d3d9f473 got back to import.meta (so dev version works) 2025-01-10 15:45:07 -05:00
C. Fuhrman
3faed3625e allow always using 'src/constants' (a kind of absolute path) 2025-01-10 15:44:22 -05:00
C. Fuhrman
535e726e6e use correct env variable 2025-01-10 15:43:12 -05:00
C. Fuhrman
3366fbe18c map the constants import to a mocked one 2025-01-10 15:42:47 -05:00
Christopher (Cris) Fuhrman
0a7f507a47
Merge pull request #180 from ets-cfuhrman-pfe/JubaAzul/issue179
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
Login affiche "Nom de la salle" plutôt que "Mot de passe" et "Nom d'utilisateur" au lieu d' "Adresse courriel"
2025-01-10 13:39:44 -05:00
JubaAzul
81eedfbd29 Login affiche "Nom de la salle" plutôt que "Mot de passe" et "Nom d'utilisateur" au lieu d' "Adresse courriel"
Fixes #179
2025-01-10 13:20:40 -05:00
JubaAzul
ebc0c64ef4 Added tests 2025-01-10 12:39:52 -05:00
C. Fuhrman
4959e02acf Fix broken merge, npm audit fix 2025-01-10 11:09:06 -05:00
Christopher (Cris) Fuhrman
e0ac770230
Merge branch 'main' into fuhrmanator/issue78 2025-01-10 11:01:29 -05:00
Christopher (Cris) Fuhrman
f4e21ee7a9
Merge pull request #175 from ets-cfuhrman-pfe/JubaAzul/issue170
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run
LaTeX mal affichée quand on lance le quiz (mais ok en Prévisualisation)
2025-01-09 16:30:09 -05:00
Christopher (Cris) Fuhrman
ecf4f9a819
Merge pull request #174 from ets-cfuhrman-pfe/JubaAzul/issue74
Ajustement de l'interface problématique dans l'éditeur de quiz
2025-01-09 16:28:57 -05:00
JubaAzul
ebd6101a64 LaTeX mal affichée quand on lance le quiz (mais ok en Prévisualisation)
Fixes #170
2025-01-08 16:45:48 -05:00
JubaAzul
c3de76cd20 Ajustement de l'interface problématique dans l'éditeur de quiz
Fixes #74
2025-01-08 13:48:03 -05:00
Christopher (Cris) Fuhrman
b46c1c1934
Merge pull request #172 from ets-cfuhrman-pfe/JubaAzul/issue146
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Icône pour dupliquer est l'icône pour copier (presse papiers)
2025-01-08 10:39:47 -05:00
JubaAzul
5c64f60021 Icône pour dupliquer est l'icône pour copier (presse papiers)
Fixes #146
2025-01-08 10:35:56 -05:00
Christopher (Cris) Fuhrman
f2fe6031bb
Merge pull request #162 from ets-cfuhrman-pfe/ssl-changes
Clean up ENV variables, default to window.location.host when no socket url
2024-10-31 11:36:35 -04:00
C. Fuhrman
3712464873 Clean up ENV variables
Define one for SOCKET url (testing), if it's not defined socket.io defaults to window.location.host
2024-10-31 11:31:26 -04:00
Christopher (Cris) Fuhrman
c754d71623
Merge pull request #161 from ets-cfuhrman-pfe/ssl-changes
Change the way the back-end url is set to a socket url
2024-10-31 01:03:54 -04:00
C. Fuhrman
e2c4e5cba2 look at protocol in the env variable 2024-10-31 01:00:26 -04:00
C. Fuhrman
e113e2f9e9 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into ssl-changes 2024-10-31 00:02:16 -04:00
C. Fuhrman
327875dceb make sure backend env is set for client 2024-10-30 23:58:56 -04:00
Christopher (Cris) Fuhrman
00c0abdf73
Merge pull request #160 from ets-cfuhrman-pfe/ssl-changes
Ssl changes
2024-10-30 23:44:17 -04:00
C. Fuhrman
ed78dc12ef be sure to get the right variable 2024-10-30 23:42:21 -04:00
C. Fuhrman
d75850dac4 remove obsolete version info 2024-10-30 23:41:55 -04:00
Christopher (Cris) Fuhrman
92b97681fc
Merge pull request #159 from ets-cfuhrman-pfe/ssl-changes
Enable source maps for Vite (frontend) to help debugging in the browser.
2024-10-30 22:06:06 -04:00
C. Fuhrman
e04d09b8c5 npm update and set rollupOptions in vite config
The goal is to prevent errors with source maps
2024-10-30 22:04:48 -04:00
C. Fuhrman
123c49662f Enable source maps for Vite (frontend) to help debugging in the browser. 2024-10-30 21:40:35 -04:00
Christopher (Cris) Fuhrman
4181a73a7e
Merge pull request #157 from ets-cfuhrman-pfe/ssl-changes
Ssl changes (debugging, trying to make wss work in prod)
2024-10-30 17:23:43 -04:00
C. Fuhrman
8be2efbe48 try to force the correct url (ws: or wss:) on the socket when it's created 2024-10-30 17:20:13 -04:00
C. Fuhrman
46dae02d47 Console.log debug info 2024-10-30 17:19:11 -04:00
C. Fuhrman
d57c61f78f Nom du dossier pour chaque quiz devrait être affiché lorsque "tous les dossiers" sont affichés
Fixes #78
Uses Cards in Material UI to display the quizzes by folder
2024-10-19 22:58:49 -04:00
101 changed files with 8045 additions and 3598 deletions

View file

@ -21,9 +21,13 @@ jobs:
with: with:
node-version: '18' node-version: '18'
- name: Install Dependencies and Run Tests - name: Install Dependencies, lint and Run Tests
run: | run: |
echo "Installing dependencies..."
npm ci npm ci
echo "Running ESLint..."
npx eslint .
echo "Running tests..."
npm test npm test
working-directory: ${{ matrix.directory }} working-directory: ${{ matrix.directory }}

2
.gitignore vendored
View file

@ -73,7 +73,7 @@ web_modules/
.yarn-integrity .yarn-integrity
# dotenv environment variable files # dotenv environment variable files
.env server/.env
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local

View file

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,2 +1 @@
**/node_modules **/node_modules
.env

2
client/.env.development Normal file
View file

@ -0,0 +1,2 @@
VITE_BACKEND_URL=http://localhost:4400
VITE_BACKEND_SOCKET_URL=http://localhost:4400

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, env: { browser: true, es2020: true },

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = { module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'] presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
}; };

29
client/eslint.config.js Normal file
View file

@ -0,0 +1,29 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: globals.browser,
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
settings: {
react: {
version: "detect", // Automatically detect the React version
},
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
@ -11,7 +12,11 @@ module.exports = {
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], //moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./jest.setup.cjs'], setupFiles: ['./jest.setup.cjs'],
moduleNameMapper: { moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy' '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// Permet de mocker les constantes pour les tests avec un chemin absolue (ex: import { ENV_VARIABLES } from 'src/constants';). Voir les "paths" dans tsconfig.json.
'^src/constants$': '<rootDir>/src/__mocks__/constantsMock.tsx',
// Dû au fait que tous les imports de "src/" sont normalisés, Jest doit comprendre le chemin réel. TODO: Trouver une solution pour que Jest se fie à tsconfig.json.
'^src/(.*)$': '<rootDir>/src/$1',
}, },
transformIgnorePatterns: ['node_modules/(?!nanoid/)'], transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
}; };

View file

@ -1,7 +1,3 @@
global.import = { /* eslint-disable no-undef */
meta: { process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
env: { process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
}
}
};

7236
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
"@mui/material": "^6.1.0", "@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"axios": "^1.6.7", "axios": "^1.6.7",
"dompurify": "^3.2.3",
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2", "gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
@ -43,6 +44,7 @@
"@babel/preset-env": "^7.23.3", "@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3", "@babel/preset-typescript": "^7.23.3",
"@eslint/js": "^9.18.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0", "@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1", "@testing-library/react": "^16.0.1",
@ -54,13 +56,16 @@
"@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0", "@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^9.10.0", "eslint": "^9.18.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912", "eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.12", "eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.5", "vite": "^5.4.5",
"vite-plugin-environment": "^1.1.3" "vite-plugin-environment": "^1.1.3"
} }

View file

@ -1,3 +1,4 @@
import React from 'react';
// App.tsx // App.tsx
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';

View file

@ -2,6 +2,7 @@
export interface QuizType { export interface QuizType {
_id: string; _id: string;
folderId: string; folderId: string;
folderName: string;
userId: string; userId: string;
title: string; title: string;
content: string[]; content: string[];

View file

@ -0,0 +1,13 @@
console.log('constantsMock.tsx is loaded');
// constants.tsx
const ENV_VARIABLES = {
MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
VITE_BACKEND_SOCKET_URL: process.env.VITE_BACKEND_SOCKET_URL || "",
};
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES };

View file

@ -9,6 +9,7 @@ describe('isQuizValid function', () => {
const validQuiz: QuizType = { const validQuiz: QuizType = {
_id: '1', _id: '1',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),
updated_at: new Date('2021-10-02'), updated_at: new Date('2021-10-02'),
@ -24,6 +25,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = { const invalidQuiz: QuizType = {
_id: '2', _id: '2',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
title: '', title: '',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),
@ -39,6 +41,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = { const invalidQuiz: QuizType = {
_id: '2', _id: '2',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
title: 'sample', title: 'sample',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),

View file

@ -1,7 +1,8 @@
// Modal.test.tsx // Modal.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react'; import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog'; import ConfirmDialog from 'src/components/ConfirmDialog/ConfirmDialog';
describe('ConfirmDialog Component', () => { describe('ConfirmDialog Component', () => {
const mockOnConfirm = jest.fn(); const mockOnConfirm = jest.fn();

View file

@ -1,7 +1,8 @@
// Editor.test.tsx // Editor.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react'; import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import Editor from '../../../components/Editor/Editor'; import Editor from 'src/components/Editor/Editor';
describe('Editor Component', () => { describe('Editor Component', () => {
const mockOnEditorChange = jest.fn(); const mockOnEditorChange = jest.fn();

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview'; import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
describe('GIFTTemplatePreview Component', () => { describe('GIFTTemplatePreview Component', () => {
test('renders error message when questions contain invalid syntax', () => { test('renders error message when questions contain invalid syntax', () => {

View file

@ -1,7 +1,7 @@
// TextType.test.ts // TextType.test.ts
import { TextFormat } from "gift-pegjs"; import { TextFormat } from "gift-pegjs";
import textType from "../../../components/GiftTemplate/templates/TextType"; import textType from "src/components/GiftTemplate/templates/TextType";
describe('TextType', () => { describe('TextType', () => {
it('should format text with basic characters correctly', () => { it('should format text with basic characters correctly', () => {
@ -32,7 +32,7 @@ describe('TextType', () => {
// Hint -- if the output changes because of a change in the code or library, you can update // Hint -- if the output changes because of a change in the code or library, you can update
// by running the test and copying the "Received string:" in jest output // by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct) // when it fails (assuming the output is correct)
const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>'; const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });
@ -42,19 +42,17 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
// hint: katex-display is the class that indicates a separate equation // hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>'; const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });
it('should format text with a katex matrix correctly', () => { it('should format text with a katex matrix correctly', () => {
const input: TextFormat = { const input: TextFormat = {
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\} // eslint-disable-next-line no-useless-escape
a&b \\\\ text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`,
c&d
\\end\{pmatrix\}`,
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow></mrow><annotation encoding=\"application/x-tex\"></annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}'; const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow></mrow><annotation encoding="application/x-tex"></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
expect(textType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });

View file

@ -1,5 +1,5 @@
//color.test.tsx //color.test.tsx
import { colors } from "../../../../components/GiftTemplate/constants"; import { colors } from "src/components/GiftTemplate/constants";
describe('Colors object', () => { describe('Colors object', () => {
test('All colors are defined', () => { test('All colors are defined', () => {

View file

@ -1,8 +1,9 @@
//styles.test.tsx //styles.test.tsx
import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants'; import { ParagraphStyle } from 'src/components/GiftTemplate/constants';
describe('ParagraphStyle', () => { describe('ParagraphStyle', () => {
test('applies styles correctly', () => { test('applies styles correctly', () => {
@ -27,6 +28,7 @@ function convertStylesToObject(styles: string): React.CSSProperties {
styles.split(';').forEach((style) => { styles.split(';').forEach((style) => {
const [property, value] = style.split(':'); const [property, value] = style.split(':');
if (property && value) { if (property && value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(styleObject as any)[property.trim()] = value.trim(); (styleObject as any)[property.trim()] = value.trim();
} }
}); });

View file

@ -1,6 +1,6 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { theme } from '../../../../components/GiftTemplate/constants/theme'; import { theme } from 'src/components/GiftTemplate/constants/theme';
import { colors } from '../../../../components/GiftTemplate/constants/colors'; import { colors } from 'src/components/GiftTemplate/constants/colors';
describe('Theme', () => { describe('Theme', () => {
test('returns correct light color', () => { test('returns correct light color', () => {

View file

@ -1,10 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon'; import AnswerIcon from 'src/components/GiftTemplate/templates/AnswerIcon';
import DOMPurify from 'dompurify';
describe('AnswerIcon', () => { describe('AnswerIcon', () => {
test('renders correct icon when correct is true', () => { test('renders correct icon when correct is true', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />); const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: true })) }} />);
const svgElement = container.querySelector('svg'); const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument(); expect(svgElement).toBeInTheDocument();
@ -19,7 +21,7 @@ describe('AnswerIcon', () => {
}); });
test('renders incorrect icon when correct is false', () => { test('renders incorrect icon when correct is false', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />); const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: false })) }} />);
const svgElement = container.querySelector('svg'); const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument(); expect(svgElement).toBeInTheDocument();

View file

@ -0,0 +1,133 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MultipleChoice } from 'src/components/GiftTemplate/templates';
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from 'src/components/GiftTemplate/templates/types';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const mockProps: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'plain' , text: 'Sample Stem'},
choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const katekMock: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'plain', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
const mockMoodle: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'moodle' , text: 'Sample Stem'},
choices: [
{ text: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const mockHTML: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [
{ text: { format: 'html' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'html', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const mockMarkdown: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'markdown', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
};
const mockMarkdownTwoImages: TemplateOptions & MultipleChoiceType = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt = "Sample Image"/>' },
choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
};
test('MultipleChoice snapshot test', () => {
const { asFragment } = render(<MultipleChoice {...mockProps} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with katex', () => {
const { asFragment } = render(<MultipleChoice {...katekMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with image', () => {
const { asFragment } = render(<MultipleChoice {...imageMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with Moodle text format', () => {
const { asFragment } = render(<MultipleChoice {...mockMoodle} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with katex, using html text format', () => {
const { asFragment } = render(<MultipleChoice {...mockHTML} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with image using markdown text format', () => {
const { asFragment } = render(<MultipleChoice {...mockMarkdown} />);
expect(asFragment()).toMatchSnapshot();
});
test('MultipleChoice snapshot test with 2 images using markdown text format', () => {
const { asFragment } = render(<MultipleChoice {...mockMarkdownTwoImages} />);
expect(asFragment()).toMatchSnapshot();
});

View file

@ -0,0 +1,79 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import Numerical from 'src/components/GiftTemplate/templates/Numerical';
import { TemplateOptions, Numerical as NumericalType } from 'src/components/GiftTemplate/templates/types';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'plain', text: 'Sample Stem' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const htmlMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'html', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'html', text: 'Incorrect!' } }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'moodle', text: 'Sample Stem' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'moodle', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'moodle', text: 'Incorrect!' } }
],
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 44}, feedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' } }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
test('Numerical snapshot test with plain text', () => {
const { asFragment } = render(<Numerical {...plainTextMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('Numerical snapshot test with html', () => {
const { asFragment } = render(<Numerical {...htmlMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('Numerical snapshot test with moodle', () => {
const { asFragment } = render(<Numerical {...moodleMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('Numerical snapshot test with image', () => {
const { asFragment } = render(<Numerical {...imageMock} />);
expect(asFragment()).toMatchSnapshot();
});

View file

@ -0,0 +1,80 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswer';
import { TemplateOptions, ShortAnswer as ShortAnswerType } from 'src/components/GiftTemplate/templates/types';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'plain', text: 'Sample Stem' },
choices: [
{ text: { format: 'plain' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const katexMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
choices: [
{ text: { format: 'html' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'html' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'html' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'moodle' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'moodle', text: 'Sample Stem' },
choices: [
{ text: { format: 'moodle' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'moodle' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title with Image',
stem: { format: 'markdown', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'markdown', text: 'Answer 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Answer 2' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
test('ShortAnswer snapshot test with plain text', () => {
const { asFragment } = render(<ShortAnswer {...plainTextMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('ShortAnswer snapshot test with katex', () => {
const { asFragment } = render(<ShortAnswer {...katexMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('ShortAnswer snapshot test with moodle', () => {
const { asFragment } = render(<ShortAnswer {...moodleMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('ShortAnswer snapshot test with image', () => {
const { asFragment } = render(<ShortAnswer {...imageMock} />);
expect(asFragment()).toMatchSnapshot();
});

View file

@ -0,0 +1,74 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalse from 'src/components/GiftTemplate/templates';
import { TemplateOptions, TrueFalse as TrueFalseType } from 'src/components/GiftTemplate/templates/types';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'plain', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'plain', text: 'Correct!' },
falseFeedback: { format: 'plain', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const katexMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'html', text: 'Incorrect!' },
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'moodle', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
trueFeedback: { format: 'moodle', text: 'Correct!' },
isTrue: true,
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }
};
test('TrueFalse snapshot test with plain text', () => {
const { asFragment } = render(<TrueFalse {...plainTextMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with katex', () => {
const { asFragment } = render(<TrueFalse {...katexMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with moodle', () => {
const { asFragment } = render(<TrueFalse {...moodleMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with image', () => {
const { asFragment } = render(<TrueFalse {...imageMock} />);
expect(asFragment()).toMatchSnapshot();
});

View file

@ -0,0 +1,301 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Numerical snapshot test with html 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43, 44"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -0,0 +1,304 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShortAnswer snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image
&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="Answer 1
, Answer 2
, &lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with katex 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -0,0 +1,438 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TrueFalse snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with katex 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback
&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import DragAndDrop from '../../../components/ImportModal/ImportModal'; import DragAndDrop from 'src/components/ImportModal/ImportModal';
describe('DragAndDrop Component', () => { describe('DragAndDrop Component', () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog'; import LaunchQuizDialog from 'src/components/LaunchQuizDialog/LaunchQuizDialog';
// Mock the functions passed as props // Mock the functions passed as props
const mockHandleOnClose = jest.fn(); const mockHandleOnClose = jest.fn();

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
describe('LoadingCircle', () => { describe('LoadingCircle', () => {
it('displays the provided text correctly', () => { it('displays the provided text correctly', () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion'; import MultipleChoiceQuestion from 'src/components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import { act } from 'react'; import { act } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';

View file

@ -1,7 +1,8 @@
// NumericalQuestion.test.tsx // NumericalQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion'; import NumericalQuestion from 'src/components/Questions/NumericalQuestion/NumericalQuestion';
describe('NumericalQuestion Component', () => { describe('NumericalQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();

View file

@ -1,7 +1,8 @@
// Question.test.tsx // Question.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import Questions from '../../../components/Questions/Question'; import Questions from 'src/components/Questions/Question';
import { GIFTQuestion } from 'gift-pegjs'; import { GIFTQuestion } from 'gift-pegjs';
// //

View file

@ -1,7 +1,8 @@
// ShortAnswerQuestion.test.tsx // ShortAnswerQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion'; import ShortAnswerQuestion from 'src/components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
describe('ShortAnswerQuestion Component', () => { describe('ShortAnswerQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();
@ -11,6 +12,7 @@ describe('ShortAnswerQuestion Component', () => {
questionTitle: 'Sample Question', questionTitle: 'Sample Question',
choices: [ choices: [
{ {
id: '1',
feedback: { feedback: {
format: 'text', format: 'text',
text: 'Correct answer feedback' text: 'Correct answer feedback'
@ -22,6 +24,7 @@ describe('ShortAnswerQuestion Component', () => {
} }
}, },
{ {
id: '2',
feedback: null, feedback: null,
isCorrect: false, isCorrect: false,
text: { text: {
@ -58,7 +61,7 @@ describe('ShortAnswerQuestion Component', () => {
expect(submitButton).toBeDisabled(); expect(submitButton).toBeDisabled();
}); });
it('not submited answer if nothing is entered', () => { it('not submitted answer if nothing is entered', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); fireEvent.click(submitButton);

View file

@ -1,7 +1,8 @@
// TrueFalseQuestion.test.tsx // TrueFalseQuestion.test.tsx
import React from 'react';
import { render, fireEvent, screen, act } from '@testing-library/react'; import { render, fireEvent, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion'; import TrueFalseQuestion from 'src/components/Questions/TrueFalseQuestion/TrueFalseQuestion';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
describe('TrueFalseQuestion Component', () => { describe('TrueFalseQuestion Component', () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ReturnButton from '../../../components/ReturnButton/ReturnButton'; import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({

View file

@ -1,7 +1,8 @@
// Importez le type UserType s'il n'est pas déjà importé // Importez le type UserType s'il n'est pas déjà importé
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage'; import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import { StudentType, Answer } from '../../../Types/StudentType'; import { StudentType, Answer } from '../../../Types/StudentType';
describe('StudentWaitPage Component', () => { describe('StudentWaitPage Component', () => {

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react'; import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';

View file

@ -1,9 +1,10 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react'; import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { QuestionType } from '../../../../Types/QuestionType'; import { QuestionType } from '../../../../Types/QuestionType';
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
const mockGiftQuestions = parse( const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B} `::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
@ -76,21 +77,5 @@ describe('StudentModeQuiz', () => {
}); });
test('navigates to the previous question', async () => {
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
act(() => {
fireEvent.click(screen.getByText('Question précédente'));
});
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
});
}); });

View file

@ -1,10 +1,11 @@
//TeacherModeQuiz.test.tsx //TeacherModeQuiz.test.tsx
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react'; import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
// import { mock } from 'node:test'; // import { mock } from 'node:test';

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';

View file

@ -31,8 +31,8 @@ Object.defineProperty(window, 'localStorage', {
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database) // NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
describe.skip('QuizService', () => { describe.skip('QuizService', () => {
const mockQuizzes: QuizType[] = [ const mockQuizzes: QuizType[] = [
{ folderId: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') }, { folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
{ folderId: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') }, { folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
]; ];
beforeEach(() => { beforeEach(() => {

View file

@ -1,16 +1,10 @@
//WebsocketService.test.tsx //WebsocketService.test.tsx
import WebsocketService from '../../services/WebsocketService'; import WebsocketService from '../../services/WebsocketService';
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../constants'; import { ENV_VARIABLES } from 'src/constants';
jest.mock('socket.io-client'); jest.mock('socket.io-client');
jest.mock('../../constants', () => ({
ENV_VARIABLES: {
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
}
}));
describe('WebSocketService', () => { describe('WebSocketService', () => {
let mockSocket: Partial<Socket>; let mockSocket: Partial<Socket>;
@ -29,13 +23,13 @@ describe('WebSocketService', () => {
}); });
test('connect should initialize socket connection', () => { test('connect should initialize socket connection', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(io).toHaveBeenCalled(); expect(io).toHaveBeenCalled();
expect(WebsocketService['socket']).toBe(mockSocket); expect(WebsocketService['socket']).toBe(mockSocket);
}); });
test('disconnect should terminate socket connection', () => { test('disconnect should terminate socket connection', () => {
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(WebsocketService['socket']).toBeTruthy(); expect(WebsocketService['socket']).toBeTruthy();
WebsocketService.disconnect(); WebsocketService.disconnect();
expect(mockSocket.disconnect).toHaveBeenCalled(); expect(mockSocket.disconnect).toHaveBeenCalled();
@ -43,7 +37,7 @@ describe('WebSocketService', () => {
}); });
test('createRoom should emit create-room event', () => { test('createRoom should emit create-room event', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.createRoom(); WebsocketService.createRoom();
expect(mockSocket.emit).toHaveBeenCalledWith('create-room'); expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
}); });
@ -52,7 +46,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
const question = { id: 1, text: 'Sample Question' }; const question = { id: 1, text: 'Sample Question' };
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.nextQuestion(roomName, question); WebsocketService.nextQuestion(roomName, question);
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question }); expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
}); });
@ -61,7 +55,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
const questions = [{ id: 1, text: 'Sample Question' }]; const questions = [{ id: 1, text: 'Sample Question' }];
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.launchStudentModeQuiz(roomName, questions); WebsocketService.launchStudentModeQuiz(roomName, questions);
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', { expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
roomName, roomName,
@ -72,7 +66,7 @@ describe('WebSocketService', () => {
test('endQuiz should emit end-quiz event with correct parameters', () => { test('endQuiz should emit end-quiz event with correct parameters', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.endQuiz(roomName); WebsocketService.endQuiz(roomName);
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName }); expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
}); });
@ -81,7 +75,7 @@ describe('WebSocketService', () => {
const enteredRoomName = 'testRoom'; const enteredRoomName = 'testRoom';
const username = 'testUser'; const username = 'testUser';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.joinRoom(enteredRoomName, username); WebsocketService.joinRoom(enteredRoomName, username);
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username }); expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
}); });

View file

@ -1,4 +1,5 @@
// GoBackButton.tsx // GoBackButton.tsx
import React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog'; import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -33,7 +34,7 @@ const DisconnectButton: React.FC<Props> = ({
}; };
const handleOnReturn = () => { const handleOnReturn = () => {
if (!!onReturn) { if (onReturn) {
onReturn(); onReturn();
} else { } else {
navigate(-1); navigate(-1);

View file

@ -1,20 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import './footer.css'; import './footer.css';
interface FooterProps { type FooterProps = object; //empty object
} const Footer: React.FC<FooterProps> = () => {
const Footer: React.FC<FooterProps> = ({ }) => {
return ( return (
<div className="footer"> <div className="footer">
<div className="footer-content"> <div className="footer-content">
Réalisé avec à Montréal par des finissantes de l'ETS Réalisé avec à Montréal par des finissantes de l&apos;ETS
</div> </div>
<div className="footer-links"> <div className="footer-links">
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a> <a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
<span className="divider">|</span> <span className="divider">|</span>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a> <a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
</div> </div>

View file

@ -21,16 +21,16 @@ const GiftCheatSheet: React.FC = () => {
}; };
const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}"; const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}// La bonne réponse est Ottawa"; const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa";
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez le signe ~ pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse."; const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n/ Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte."; const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n/ Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} // OU \nQuestion {#=PetitNombre..GrandNombre\n}\n// La tolérance est un pourcentage.\n// La réponse doit être comprise entre PetitNombre et GrandNombre"; const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
return ( return (
<div className="gift-cheat-sheet"> <div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2> <h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
<span> <span>
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la L&apos;éditeur utilise le format GIFT (General Import Format Template) créé pour la
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
syntaxe pour chaque type de question&nbsp;: syntaxe pour chaque type de question&nbsp;:
</span> </span>
@ -126,7 +126,7 @@ const GiftCheatSheet: React.FC = () => {
<h4> 7. Paramètres optionnels </h4> <h4> 7. Paramètres optionnels </h4>
<p> <p>
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés, Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
réponses ou feedback, vous devez 'échapper' ces derniers en ajoutant un \ réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
devant: devant:
</p> </p>
<pre> <pre>
@ -140,9 +140,9 @@ const GiftCheatSheet: React.FC = () => {
<h4> 8. LaTeX et Markdown</h4> <h4> 8. LaTeX et Markdown</h4>
<p> <p>
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
à 'échapper' les caractères spéciaux mentionnés plus haut. à «échapper» les caractères spéciaux mentionnés plus haut.
</p> </p>
<p>Exemple d'équation:</p> <p>Exemple d&apos;équation:</p>
<pre> <pre>
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code> <code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code> <code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
@ -167,16 +167,16 @@ const GiftCheatSheet: React.FC = () => {
{'")'} {'")'}
</code> </code>
</pre> </pre>
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p> <p>Exemple d&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p>
<pre> <pre>
<code className="question-code-block"> <code className="question-code-block">
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'} {'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code> </code>
</pre> </pre>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p> <p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l&apos;URL de l&apos;image.</p>
<p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p> <p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
<p style={{ color: 'red' }}> <p style={{ color: 'red' }}>
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
supportée. supportée.
</p> </p>
</div> </div>
@ -184,7 +184,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type"> <div className="question-type">
<h4> 10. Informations supplémentaires </h4> <h4> 10. Informations supplémentaires </h4>
<p> <p>
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette GIFT supporte d&apos;autres formats de questions que nous ne gérons pas sur cette
application. application.
</p> </p>
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p> <p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>

View file

@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import './styles.css'; import './styles.css';
import DOMPurify from 'dompurify';
interface GIFTTemplatePreviewProps { interface GIFTTemplatePreviewProps {
questions: string[]; questions: string[];
@ -73,7 +74,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div> <div className="error">{error}</div>
) : isPreviewReady ? ( ) : isPreviewReady ? (
<div data-testid="preview-container"> <div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: items }}></div> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
</div> </div>
) : ( ) : (
<div className="loading">Chargement de la prévisualisation...</div> <div className="loading">Chargement de la prévisualisation...</div>

View file

@ -30,7 +30,7 @@ export function formatLatex(text: string): string {
*/ */
export default function textType({ text }: TextTypeOptions) { export default function textType({ text }: TextTypeOptions) {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = '';
switch (text.format) { switch (text.format) {
case 'moodle': case 'moodle':
case 'plain': case 'plain':
@ -40,7 +40,7 @@ export default function textType({ text }: TextTypeOptions) {
// Strip outer paragraph tags (not a great approach with regex) // Strip outer paragraph tags (not a great approach with regex)
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2'); return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
case 'markdown': case 'markdown':
const parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219 parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2'); return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
default: default:
throw new Error(`Unsupported text format: ${text.format}`); throw new Error(`Unsupported text format: ${text.format}`);

View file

@ -168,7 +168,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
<DialogContentText sx={{ textAlign: 'center' }}> <DialogContentText sx={{ textAlign: 'center' }}>
Déposer des fichiers ici ou Déposer des fichiers ici ou
<br /> <br />
cliquez pour ouvrir l'explorateur des fichiers cliquez pour ouvrir l&apos;explorateur des fichiers
</DialogContentText> </DialogContentText>
</div> </div>
<Download color="primary" /> <Download color="primary" />

View file

@ -1,3 +1,4 @@
import React from 'react';
import { import {
Button, Button,
Dialog, Dialog,

View file

@ -300,7 +300,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell className="sticky-column"> <TableCell className="sticky-column">
<div className="text-base text-bold">Nom d'utilisateur</div> <div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell> </TableCell>
{Array.from({ length: maxQuestions }, (_, index) => ( {Array.from({ length: maxQuestions }, (_, index) => (
<TableCell <TableCell

View file

@ -1,3 +1,4 @@
import React from 'react';
import { IconButton } from '@mui/material'; import { IconButton } from '@mui/material';
import { ChevronLeft, ChevronRight } from '@mui/icons-material'; import { ChevronLeft, ChevronRight } from '@mui/icons-material';

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType'; import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
// import Latex from 'react-latex'; // import Latex from 'react-latex';
type Choices = { type Choices = {
@ -22,6 +23,7 @@ interface Props {
} }
const MultipleChoiceQuestion: React.FC<Props> = (props) => { const MultipleChoiceQuestion: React.FC<Props> = (props) => {
const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props; const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const [answer, setAnswer] = useState<string>(); const [answer, setAnswer] = useState<string>();
@ -39,7 +41,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{choices.map((choice, i) => { {choices.map((choice, i) => {
@ -56,7 +58,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')} (choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
{formatLatex(choice.text.text)} <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} />
</div> </div>
</Button> </Button>
{choice.feedback && showAnswer && ( {choice.feedback && showAnswer && (
@ -72,7 +74,9 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
{globalFeedback && showAnswer && ( {globalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div> <div className="global-feedback mb-2">{globalFeedback}</div>
)} )}
{!showAnswer && handleOnSubmitAnswer && ( {!showAnswer && handleOnSubmitAnswer && (
<Button <Button
variant="contained" variant="contained"
onClick={() => onClick={() =>
@ -81,6 +85,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
disabled={answer === undefined} disabled={answer === undefined}
> >
Répondre Répondre
</Button> </Button>
)} )}
</div> </div>

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type CorrectAnswer = { type CorrectAnswer = {
numberHigh?: number; numberHigh?: number;
@ -34,7 +35,7 @@ const NumericalQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div> <div>
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>

View file

@ -42,7 +42,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = ( questionTypeComponent = (
<MultipleChoiceQuestion <MultipleChoiceQuestion
questionStem={question.stem} questionStem={question.stem}
choices={question.choices} choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text} globalFeedback={question.globalFeedback?.text}
@ -78,7 +78,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = ( questionTypeComponent = (
<ShortAnswerQuestion <ShortAnswerQuestion
questionContent={question.stem} questionContent={question.stem}
choices={question.choices} choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text} globalFeedback={question.globalFeedback?.text}

View file

@ -4,12 +4,14 @@ import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type Choices = { type Choices = {
feedback: { format: string; text: string } | null; feedback: { format: string; text: string } | null;
isCorrect: boolean; isCorrect: boolean;
text: { format: string; text: string }; text: { format: string; text: string };
weigth?: number; weigth?: number;
id: string;
}; };
interface Props { interface Props {
@ -27,13 +29,15 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>
<div className="correct-answer-text mb-1"> <div className="correct-answer-text mb-1">
{choices.map((choice) => ( {choices.map((choice) => (
<div className="mb-1">{choice.text.text}</div> <div key={choice.id} className="mb-1">
{choice.text.text}
</div>
))} ))}
</div> </div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>} {globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
interface Props { interface Props {
questionContent: TextFormat; questionContent: TextFormat;
@ -27,7 +28,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button

View file

@ -1,4 +1,5 @@
// GoBackButton.tsx // GoBackButton.tsx
import React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog'; import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -33,7 +34,7 @@ const ReturnButton: React.FC<Props> = ({
}; };
const handleOnReturn = () => { const handleOnReturn = () => {
if (!!onReturn) { if (onReturn) {
onReturn(); onReturn();
} else { } else {
navigate(-1); navigate(-1);

View file

@ -1,14 +1,13 @@
// StudentModeQuiz.tsx // StudentModeQuiz.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import QuestionComponent from '../Questions/Question'; import QuestionComponent from '../Questions/Question';
import '../../pages/Student/JoinRoom/joinRoom.css'; import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService'; // import { QuestionService } from '../../services/QuestionService';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import QuestionNavigation from '../QuestionNavigation/QuestionNavigation'; //import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
import { ChevronLeft, ChevronRight } from '@mui/icons-material'; //import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
interface StudentModeQuizProps { interface StudentModeQuizProps {
questions: QuestionType[]; questions: QuestionType[];
@ -25,10 +24,10 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
// const [imageUrl, setImageUrl] = useState(''); // const [imageUrl, setImageUrl] = useState('');
const previousQuestion = () => { // const previousQuestion = () => {
setQuestion(questions[Number(questionInfos.question?.id) - 2]); // setQuestion(questions[Number(questionInfos.question?.id) - 2]);
setIsAnswerSubmitted(false); // setIsAnswerSubmitted(false);
}; // };
useEffect(() => {}, [questionInfos]); useEffect(() => {}, [questionInfos]);
@ -55,12 +54,12 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
<div className="overflow-auto"> <div className="overflow-auto">
<div className="question-component-container"> <div className="question-component-container">
<div className="mb-5"> <div className="mb-5">
<QuestionNavigation {/* <QuestionNavigation
currentQuestionId={Number(questionInfos.question.id)} currentQuestionId={Number(questionInfos.question.id)}
questionsLength={questions.length} questionsLength={questions.length}
previousQuestion={previousQuestion} previousQuestion={previousQuestion}
nextQuestion={nextQuestion} nextQuestion={nextQuestion}
/> /> */}
</div> </div>
<QuestionComponent <QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
@ -69,7 +68,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
/> />
<div className="center-h-align mt-1/2"> <div className="center-h-align mt-1/2">
<div className="w-12"> <div className="w-12">
<Button {/* <Button
variant="outlined" variant="outlined"
onClick={previousQuestion} onClick={previousQuestion}
fullWidth fullWidth
@ -77,14 +76,14 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
disabled={Number(questionInfos.question.id) <= 1} disabled={Number(questionInfos.question.id) <= 1}
> >
Question précédente Question précédente
</Button> </Button> */}
</div> </div>
<div className="w-12"> <div className="w-12">
<Button <Button style={{ display: isAnswerSubmitted ? 'block' : 'none' }}
variant="outlined" variant="outlined"
onClick={nextQuestion} onClick={nextQuestion}
fullWidth fullWidth
endIcon={<ChevronRight />} //endIcon={<ChevronRight />}
disabled={Number(questionInfos.question.id) >= questions.length} disabled={Number(questionInfos.question.id) >= questions.length}
> >
Question suivante Question suivante
@ -93,7 +92,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View file

@ -1,3 +1,4 @@
import React from 'react';
import { Box, Button, Chip } from '@mui/material'; import { Box, Button, Chip } from '@mui/material';
import { StudentType } from '../../Types/StudentType'; import { StudentType } from '../../Types/StudentType';
import { PlayArrow } from '@mui/icons-material'; import { PlayArrow } from '@mui/icons-material';

View file

@ -6,7 +6,7 @@ import QuestionComponent from '../Questions/Question';
import '../../pages/Student/JoinRoom/joinRoom.css'; import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService'; // import { QuestionService } from '../../services/QuestionService';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
interface TeacherModeQuizProps { interface TeacherModeQuizProps {

View file

@ -1,7 +1,11 @@
// constants.tsx // constants.tsx
const ENV_VARIABLES = { const ENV_VARIABLES = {
MODE: 'production', MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "" VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
}; };
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES }; export { ENV_VARIABLES };

View file

@ -1,3 +1,4 @@
import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App.tsx'; import App from './App.tsx';
@ -27,10 +28,16 @@ const theme = createTheme({
} }
}); });
ReactDOM.createRoot(document.getElementById('root')!).render( const rootElement = document.getElementById('root');
<BrowserRouter> if (rootElement) {
<ThemeProvider theme={theme}>
<App /> ReactDOM.createRoot(document.getElementById('root')!).render(
</ThemeProvider> <BrowserRouter>
</BrowserRouter> <ThemeProvider theme={theme}>
); <App />
</ThemeProvider>
</BrowserRouter>
);
} else {
console.error('Root element not found');
}

View file

@ -1,19 +1,19 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from 'src/constants';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService'; import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import './joinRoom.css'; import './joinRoom.css';
import { QuestionType } from '../../../Types/QuestionType'; import { QuestionType } from '../../../Types/QuestionType';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
const JoinRoom: React.FC = () => { const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState(''); const [roomName, setRoomName] = useState('');
@ -34,7 +34,8 @@ const JoinRoom: React.FC = () => {
}, []); }, []);
const handleCreateSocket = () => { const handleCreateSocket = () => {
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('join-success', () => { socket.on('join-success', () => {
setIsWaitingForTeacher(true); setIsWaitingForTeacher(true);
@ -63,10 +64,10 @@ const JoinRoom: React.FC = () => {
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
switch (error.message) { switch (error.message) {
case 'timeout': case 'timeout':
setConnectionError("Le serveur n'est pas disponible"); setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible");
break; break;
case 'websocket error': case 'websocket error':
setConnectionError("Le serveur n'est pas disponible"); setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible");
break; break;
} }
setIsConnecting(false); setIsConnecting(false);

View file

@ -3,14 +3,14 @@ import { useNavigate } from 'react-router-dom';
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import Template from '../../../components/GiftTemplate/templates'; import Template from 'src/components/GiftTemplate/templates';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import { FolderType } from '../../../Types/FolderType'; import { FolderType } from '../../../Types/FolderType';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import './dashboard.css'; import './dashboard.css';
import ImportModal from '../../../components/ImportModal/ImportModal'; import ImportModal from 'src/components/ImportModal/ImportModal';
//import axios from 'axios'; //import axios from 'axios';
import { import {
@ -18,8 +18,11 @@ import {
IconButton, IconButton,
InputAdornment, InputAdornment,
Button, Button,
Card,
Tooltip, Tooltip,
NativeSelect NativeSelect,
CardContent,
styled,
} from '@mui/material'; } from '@mui/material';
import { import {
Search, Search,
@ -27,19 +30,50 @@ import {
FileDownload, FileDownload,
Add, Add,
Upload, Upload,
FolderCopy,
ContentCopy, ContentCopy,
Edit, Edit,
Share, Share,
// DriveFileMove // DriveFileMove
} from '@mui/icons-material'; } from '@mui/icons-material';
// Create a custom-styled Card component
const CustomCard = styled(Card)({
overflow: 'visible', // Override the overflow property
position: 'relative',
margin: '40px 0 20px 0', // Add top margin to make space for the tab
borderRadius: '8px',
paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
});
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [quizzes, setQuizzes] = useState<QuizType[]>([]); const [quizzes, setQuizzes] = useState<QuizType[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showImportModal, setShowImportModal] = useState<boolean>(false); const [showImportModal, setShowImportModal] = useState<boolean>(false);
const [folders, setFolders] = useState<FolderType[]>([]); const [folders, setFolders] = useState<FolderType[]>([]);
const [selectedFolder, setSelectedFolder] = useState<string>(''); // Selected folder const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
// Filter quizzes based on search term
// const filteredQuizzes = quizzes.filter(quiz =>
// quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
// );
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
// Group quizzes by folder
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
if (!acc[quiz.folderName]) {
acc[quiz.folderName] = [];
}
acc[quiz.folderName].push(quiz);
return acc;
}, {} as Record<string, QuizType[]>);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -48,7 +82,7 @@ const Dashboard: React.FC = () => {
return; return;
} }
else { else {
let userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
} }
@ -58,40 +92,23 @@ const Dashboard: React.FC = () => {
fetchData(); fetchData();
}, []); }, []);
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedFolder(event.target.value); setSelectedFolderId(event.target.value);
}; };
useEffect(() => { useEffect(() => {
const fetchQuizzesForFolder = async () => { const fetchQuizzesForFolder = async () => {
if (selectedFolder == '') { if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes); console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
// add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]) quizzes = quizzes.concat(folderQuizzes as QuizType[])
} }
@ -99,17 +116,19 @@ const Dashboard: React.FC = () => {
} }
else { else {
console.log("show some quizzes") console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder); const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
console.log("folderQuizzes: ", folderQuizzes); console.log("folderQuizzes: ", folderQuizzes);
// get the folder title from its id
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
setQuizzes(folderQuizzes as QuizType[]); setQuizzes(folderQuizzes as QuizType[]);
} }
}; };
fetchQuizzesForFolder(); fetchQuizzesForFolder();
}, [selectedFolder]); }, [selectedFolderId]);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -134,22 +153,24 @@ const Dashboard: React.FC = () => {
const handleDuplicateQuiz = async (quiz: QuizType) => { const handleDuplicateQuiz = async (quiz: QuizType) => {
try { try {
await ApiService.duplicateQuiz(quiz._id); await ApiService.duplicateQuiz(quiz._id);
if (selectedFolder == '') { if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizzes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes); console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
quizzes = quizzes.concat(folderQuizzes as QuizType[]) addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
} }
setQuizzes(quizzes as QuizType[]); setQuizzes(quizzes as QuizType[]);
} }
else { else {
console.log("show some quizzes") console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder); const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
setQuizzes(folderQuizzes as QuizType[]); setQuizzes(folderQuizzes as QuizType[]);
} }
@ -158,13 +179,6 @@ const Dashboard: React.FC = () => {
} }
}; };
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
const handleOnImport = () => { const handleOnImport = () => {
setShowImportModal(true); setShowImportModal(true);
@ -182,6 +196,7 @@ const Dashboard: React.FC = () => {
// questions[i] = QuestionService.ignoreImgTags(questions[i]); // questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]); const parsedItem = parse(questions[i]);
Template(parsedItem[0]); Template(parsedItem[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
return false; return false;
} }
@ -190,30 +205,6 @@ const Dashboard: React.FC = () => {
return true; return true;
}; };
// const handleMoveQuiz = async (quiz: QuizType, newFolderId: string) => {
// try {
// await ApiService.moveQuiz(quiz._id, newFolderId);
// if (selectedFolder == '') {
// const folders = await ApiService.getUserFolders();
// var quizzes: QuizType[] = [];
// for (const folder of folders as FolderType[]) {
// const folderQuizzes = await ApiService.getFolderContent(folder._id);
// quizzes = quizzes.concat(folderQuizzes as QuizType[])
// }
// setQuizzes(quizzes as QuizType[]);
// }
// else {
// const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
// setQuizzes(folderQuizzes as QuizType[]);
// }
// } catch (error) {
// console.error('Error moving quiz:', error);
// }
// };
const downloadTxtFile = async (quiz: QuizType) => { const downloadTxtFile = async (quiz: QuizType) => {
try { try {
@ -226,7 +217,7 @@ const Dashboard: React.FC = () => {
//const { title, content } = selectedQuiz; //const { title, content } = selectedQuiz;
let quizContent = ""; let quizContent = "";
let title = selectedQuiz.title; const title = selectedQuiz.title;
console.log(selectedQuiz.content); console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => { selectedQuiz.content.forEach((question, qIndex) => {
const formattedQuestion = question.trim(); const formattedQuestion = question.trim();
@ -263,7 +254,7 @@ const Dashboard: React.FC = () => {
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType; const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id); setSelectedFolderId(newlyCreatedFolder._id);
} }
} catch (error) { } catch (error) {
@ -273,18 +264,17 @@ const Dashboard: React.FC = () => {
const handleDeleteFolder = async () => { const handleDeleteFolder = async () => {
try { try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?'); const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
if (confirmed) { if (confirmed) {
await ApiService.deleteFolder(selectedFolder); await ApiService.deleteFolder(selectedFolderId);
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
} }
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizzes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -293,19 +283,20 @@ const Dashboard: React.FC = () => {
} }
setQuizzes(quizzes as QuizType[]); setQuizzes(quizzes as QuizType[]);
setSelectedFolder(''); setSelectedFolderId('');
} catch (error) { } catch (error) {
console.error('Error deleting folder:', error); console.error('Error deleting folder:', error);
} }
}; };
const handleRenameFolder = async () => { const handleRenameFolder = async () => {
try { try {
// folderId: string GET THIS FROM CURRENT FOLDER // folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: string GET THIS FROM CURRENT FOLDER // currentTitle: string GET THIS FROM CURRENT FOLDER
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier"); const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
if (newTitle) { if (newTitle) {
await ApiService.renameFolder(selectedFolder, newTitle); await ApiService.renameFolder(selectedFolderId, newTitle);
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
@ -314,15 +305,16 @@ const Dashboard: React.FC = () => {
console.error('Error renaming folder:', error); console.error('Error renaming folder:', error);
} }
}; };
const handleDuplicateFolder = async () => { const handleDuplicateFolder = async () => {
try { try {
// folderId: string GET THIS FROM CURRENT FOLDER // folderId: string GET THIS FROM CURRENT FOLDER
await ApiService.duplicateFolder(selectedFolder); await ApiService.duplicateFolder(selectedFolderId);
// TODO set the selected folder to be the duplicated folder // TODO set the selected folder to be the duplicated folder
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType; const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id); setSelectedFolderId(newlyCreatedFolder._id);
} catch (error) { } catch (error) {
console.error('Error duplicating folder:', error); console.error('Error duplicating folder:', error);
} }
@ -392,7 +384,7 @@ const Dashboard: React.FC = () => {
<NativeSelect <NativeSelect
id="select-folder" id="select-folder"
color="primary" color="primary"
value={selectedFolder} value={selectedFolderId}
onChange={handleSelectFolder} onChange={handleSelectFolder}
> >
<option value=""> Tous les dossiers... </option> <option value=""> Tous les dossiers... </option>
@ -415,7 +407,7 @@ const Dashboard: React.FC = () => {
<IconButton <IconButton
color="primary" color="primary"
onClick={handleRenameFolder} onClick={handleRenameFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <Edit /> </IconButton> > <Edit /> </IconButton>
</Tooltip> </Tooltip>
@ -423,8 +415,8 @@ const Dashboard: React.FC = () => {
<IconButton <IconButton
color="primary" color="primary"
onClick={handleDuplicateFolder} onClick={handleDuplicateFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <ContentCopy /> </IconButton> > <FolderCopy /> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Supprimer dossier" placement="top"> <Tooltip title="Supprimer dossier" placement="top">
@ -432,7 +424,7 @@ const Dashboard: React.FC = () => {
aria-label="delete" aria-label="delete"
color="primary" color="primary"
onClick={handleDeleteFolder} onClick={handleDeleteFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <DeleteOutline /> </IconButton> > <DeleteOutline /> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
@ -460,74 +452,72 @@ const Dashboard: React.FC = () => {
</div> </div>
<div className='list'> <div className='list'>
{Object.keys(quizzesByFolder).map(folderName => (
<CustomCard key={folderName} className='folder-card'>
<div className='folder-tab'>{folderName}</div>
<CardContent>
{quizzesByFolder[folderName].map((quiz: QuizType) => (
<div className='quiz' key={quiz._id}>
<div className='title'>
<Tooltip title="Lancer quiz" placement="top">
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
</Button>
</Tooltip>
</div>
{filteredQuizzes.map((quiz: QuizType) => ( <div className='actions'>
<div className='quiz'> <Tooltip title="Télécharger quiz" placement="top">
<div className='title'> <IconButton
<Tooltip title="Lancer quiz" placement="top"> color="primary"
<Button onClick={() => downloadTxtFile(quiz)}
variant="outlined" > <FileDownload /> </IconButton>
onClick={() => handleLancerQuiz(quiz)} </Tooltip>
disabled={!validateQuiz(quiz.content)}
>
{quiz.title}
</Button>
</Tooltip>
</div>
<div className='actions'> <Tooltip title="Modifier quiz" placement="top">
<Tooltip title="Télécharger quiz" placement="top"> <IconButton
<IconButton color="primary"
color="primary" onClick={() => handleEditQuiz(quiz)}
onClick={() => downloadTxtFile(quiz)} > <Edit /> </IconButton>
> <FileDownload /> </IconButton> </Tooltip>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top"> <Tooltip title="Dupliquer quiz" placement="top">
<IconButton <IconButton
color="primary" color="primary"
onClick={() => handleEditQuiz(quiz)} onClick={() => handleDuplicateQuiz(quiz)}
> <Edit /> </IconButton> > <ContentCopy /> </IconButton>
</Tooltip> </Tooltip>
{/* <Tooltip title="Bouger quiz" placement="top"> <Tooltip title="Supprimer quiz" placement="top">
<IconButton <IconButton
color="primary" aria-label="delete"
onClick={() => handleMoveQuiz(quiz)} color="primary"
> <DriveFileMove /> </IconButton> onClick={() => handleRemoveQuiz(quiz)}
</Tooltip> */} > <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top"> <Tooltip title="Partager quiz" placement="top">
<IconButton <IconButton
color="primary" color="primary"
onClick={() => handleDuplicateQuiz(quiz)} onClick={() => handleShareQuiz(quiz)}
> <ContentCopy /> </IconButton> > <Share /> </IconButton>
</Tooltip> </Tooltip>
</div>
<Tooltip title="Supprimer quiz" placement="top"> </div>
<IconButton ))}
aria-label="delete" </CardContent>
color="primary" </CustomCard>
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
</Tooltip>
</div>
</div>
))} ))}
</div> </div>
<ImportModal <ImportModal
open={showImportModal} open={showImportModal}
handleOnClose={() => setShowImportModal(false)} handleOnClose={() => setShowImportModal(false)}
handleOnImport={handleOnImport} handleOnImport={handleOnImport}
selectedFolder={selectedFolder} selectedFolder={selectedFolderId}
/> />
</div> </div>
@ -535,3 +525,11 @@ const Dashboard: React.FC = () => {
}; };
export default Dashboard; export default Dashboard;
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
if (Array.isArray(folderQuizzes))
folderQuizzes.forEach((quiz) => {
quiz.folderName = folderName;
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
});
}

View file

@ -78,3 +78,42 @@ div:has(> #select-folder) {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.dashboard .list .quiz .actions {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.folder-card {
position: relative;
/* margin: 40px 0 20px 0; /* Add top margin to make space for the tab */
border-radius: 8px;
color: #f9f9f9;
--outline-color: #e1e1e1;
border: 2px solid var(--outline-color);
}
.folder-tab {
position: absolute;
top: -33px;
left: 9px;
padding: 5px 10px;
border-radius: 8px 8px 0 0;
font-weight: bold;
white-space: nowrap; /* Prevent text from wrapping */
display: inline-block; /* Ensure the tab width is based on content */
border: 2px solid var(--outline-color);
border-bottom-style: none;
background-color: white; /* Optional: background color to match the card */
color: #3f51b5; /* Text color to match the outline */
}
/* .folder-card:nth-child(odd) {
background-color: #f9f9f9;
}
.folder-card:nth-child(even) {
background-color: #e0e0e0;
} */

View file

@ -1,18 +1,18 @@
// EditorQuiz.tsx // EditorQuiz.tsx
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, CSSProperties } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { FolderType } from '../../../Types/FolderType'; import { FolderType } from '../../../Types/FolderType';
import Editor from '../../../components/Editor/Editor'; import Editor from 'src/components/Editor/Editor';
import GiftCheatSheet from '../../../components/GIFTCheatSheet/GiftCheatSheet'; import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview'; import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './editorQuiz.css'; import './editorQuiz.css';
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'; import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
import ReturnButton from '../../../components/ReturnButton/ReturnButton'; import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { escapeForGIFT } from '../../../utils/giftUtils'; import { escapeForGIFT } from '../../../utils/giftUtils';
@ -40,6 +40,26 @@ const QuizForm: React.FC = () => {
}; };
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 300) {
setShowScrollButton(true);
} else {
setShowScrollButton(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -162,8 +182,10 @@ const QuizForm: React.FC = () => {
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = '';
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
} }
}; };
@ -245,7 +267,7 @@ const QuizForm: React.FC = () => {
onClose={() => setDialogOpen(false)} > onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle> <DialogTitle>Erreur</DialogTitle>
<DialogContent> <DialogContent>
Veuillez d'abord choisir une image à téléverser. Veuillez d&apos;abord choisir une image à téléverser.
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary"> <Button onClick={() => setDialogOpen(false)} color="primary">
@ -290,8 +312,32 @@ const QuizForm: React.FC = () => {
</div> </div>
{showScrollButton && (
<Button
onClick={scrollToTop}
variant="contained"
color="primary"
style={scrollToTopButtonStyle}
title="Scroll to top"
>
</Button>
)}
</div> </div>
); );
}; };
const scrollToTopButtonStyle: CSSProperties = {
position: 'fixed',
bottom: '40px',
right: '50px',
padding: '10px',
fontSize: '16px',
color: 'white',
backgroundColor: '#5271ff',
border: 'none',
cursor: 'pointer',
zIndex: 1000,
};
export default QuizForm; export default QuizForm;

View file

@ -7,7 +7,7 @@ import './Login.css';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
const Login: React.FC = () => { const Login: React.FC = () => {
@ -28,7 +28,7 @@ const Login: React.FC = () => {
const login = async () => { const login = async () => {
const result = await ApiService.login(email, password); const result = await ApiService.login(email, password);
if (result != true) { if (typeof result === "string") {
setConnectionError(result); setConnectionError(result);
return; return;
} }
@ -49,7 +49,7 @@ const Login: React.FC = () => {
variant="outlined" variant="outlined"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="Nom d'utilisateur" placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth
/> />
@ -60,7 +60,7 @@ const Login: React.FC = () => {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Nom de la salle" placeholder="Mot de passe"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth
/> />

View file

@ -4,21 +4,21 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { GIFTQuestion, parse } from 'gift-pegjs'; import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType'; import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsComponent from '../../../components/LiveResults/LiveResults'; import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType'; import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material'; import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage'; import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation'; //import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import Question from '../../../components/Questions/Question'; import Question from 'src/components/Questions/Question';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
@ -49,6 +49,7 @@ const ManageRoom: React.FC = () => {
setQuiz(quiz as QuizType); setQuiz(quiz as QuizType);
if (!socket) { if (!socket) {
console.log(`no socket in ManageRoom, creating one.`);
createWebSocketRoom(); createWebSocketRoom();
} }
@ -80,15 +81,16 @@ const ManageRoom: React.FC = () => {
}; };
const createWebSocketRoom = () => { const createWebSocketRoom = () => {
console.log('Creating WebSocket room...');
setConnectingError(''); setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('connect', () => { socket.on('connect', () => {
webSocketService.createRoom(); webSocketService.createRoom();
}); });
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer'); setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
console.error('WebSocket connection error:', error); console.error('ManageRoom: WebSocket connection error:', error);
}); });
socket.on('create-success', (roomName: string) => { socket.on('create-success', (roomName: string) => {
setRoomName(roomName); setRoomName(roomName);
@ -122,8 +124,8 @@ const ManageRoom: React.FC = () => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket) {
console.log(`Listening for user-joined in room ${roomName}`); console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => { socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') { } else if (quizMode === 'student') {
@ -265,7 +267,6 @@ const ManageRoom: React.FC = () => {
const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1 const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]); setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]); webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
}; };
@ -459,12 +460,12 @@ const ManageRoom: React.FC = () => {
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
<div className="mb-1"> <div className="mb-1">
<QuestionNavigation {/* <QuestionNavigation
currentQuestionId={Number(currentQuestion?.question.id)} currentQuestionId={Number(currentQuestion?.question.id)}
questionsLength={quizQuestions?.length} questionsLength={quizQuestions?.length}
previousQuestion={previousQuestion} previousQuestion={previousQuestion}
nextQuestion={nextQuestion} nextQuestion={nextQuestion}
/> /> */}
</div> </div>
)} )}
@ -491,12 +492,23 @@ const ManageRoom: React.FC = () => {
</div> </div>
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente
</Button>
</div>
<div className="nextQuestionButton"> <div className="nextQuestionButton">
<Button onClick={nextQuestion} variant="contained"> <Button onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
>
Prochaine question Prochaine question
</Button> </Button>
</div> </div>
)} </div> )}
</div> </div>

View file

@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
const Register: React.FC = () => { const Register: React.FC = () => {
@ -28,7 +28,7 @@ const Register: React.FC = () => {
const register = async () => { const register = async () => {
const result = await ApiService.register(email, password); const result = await ApiService.register(email, password);
if (result != true) { if (typeof result === 'string') {
setConnectionError(result); setConnectionError(result);
return; return;
} }
@ -70,7 +70,7 @@ const Register: React.FC = () => {
sx={{ marginBottom: `${connectionError && '2rem'}` }} sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password} disabled={!email || !password}
> >
S'inscrire S&apos;inscrire
</LoadingButton> </LoadingButton>
</LoginContainer> </LoginContainer>

View file

@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
const ResetPassword: React.FC = () => { const ResetPassword: React.FC = () => {
@ -27,7 +27,7 @@ const ResetPassword: React.FC = () => {
const reset = async () => { const reset = async () => {
const result = await ApiService.resetPassword(email); const result = await ApiService.resetPassword(email);
if (result != true) { if (typeof result === 'string') {
setConnectionError(result); setConnectionError(result);
return; return;
} }

View file

@ -7,7 +7,7 @@ import { FolderType } from '../../../Types/FolderType';
import './share.css'; import './share.css';
import { Button, NativeSelect } from '@mui/material'; import { Button, NativeSelect } from '@mui/material';
import ReturnButton from '../../../components/ReturnButton/ReturnButton'; import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';

View file

@ -1,8 +1,10 @@
import axios, { AxiosError, AxiosResponse } from 'axios'; import axios, { AxiosError, AxiosResponse } from 'axios';
import { ENV_VARIABLES } from '../constants';
import { QuizType } from '../Types/QuizType'; import { FolderType } from 'src/Types/FolderType';
import { FolderType } from '../Types/FolderType'; import { QuizType } from 'src/Types/QuizType';
import { ENV_VARIABLES } from 'src/constants';
type ApiResponse = boolean | string;
class ApiService { class ApiService {
private BASE_URL: string; private BASE_URL: string;
@ -17,7 +19,7 @@ class ApiService {
return `${this.BASE_URL}/api${endpoint}`; return `${this.BASE_URL}/api${endpoint}`;
} }
private constructRequestHeaders(): any { private constructRequestHeaders() {
if (this.isLoggedIn()) { if (this.isLoggedIn()) {
return { return {
Authorization: `Bearer ${this.getToken()}`, Authorization: `Bearer ${this.getToken()}`,
@ -86,7 +88,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async register(email: string, password: string): Promise<any> { public async register(email: string, password: string): Promise<ApiResponse> {
try { try {
if (!email || !password) { if (!email || !password) {
@ -122,7 +124,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async login(email: string, password: string): Promise<any> { public async login(email: string, password: string): Promise<ApiResponse> {
try { try {
if (!email || !password) { if (!email || !password) {
@ -146,8 +148,13 @@ class ApiService {
} catch (error) { } catch (error) {
console.log("Error details: ", error); console.log("Error details: ", error);
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const err = error as AxiosError; const err = error as AxiosError;
if (err.status === 401) {
return 'Email ou mot de passe incorrect.';
}
const data = err.response?.data as { error: string } | undefined; const data = err.response?.data as { error: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.'; return data?.error || 'Erreur serveur inconnue lors de la requête.';
} }
@ -157,10 +164,10 @@ class ApiService {
} }
/** /**
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async resetPassword(email: string): Promise<any> { public async resetPassword(email: string): Promise<ApiResponse> {
try { try {
if (!email) { if (!email) {
@ -196,7 +203,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<any> { public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<ApiResponse> {
try { try {
if (!email || !oldPassword || !newPassword) { if (!email || !oldPassword || !newPassword) {
@ -232,7 +239,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async deleteUser(email: string, password: string): Promise<any> { public async deleteUser(email: string, password: string): Promise<ApiResponse> {
try { try {
if (!email || !password) { if (!email || !password) {
@ -270,7 +277,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async createFolder(title: string): Promise<any> { public async createFolder(title: string): Promise<ApiResponse> {
try { try {
if (!title) { if (!title) {
@ -375,7 +382,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async deleteFolder(folderId: string): Promise<any> { public async deleteFolder(folderId: string): Promise<ApiResponse> {
try { try {
if (!folderId) { if (!folderId) {
@ -410,7 +417,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async renameFolder(folderId: string, newTitle: string): Promise<any> { public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try { try {
if (!folderId || !newTitle) { if (!folderId || !newTitle) {
@ -441,7 +448,7 @@ class ApiService {
} }
} }
public async duplicateFolder(folderId: string): Promise<any> { public async duplicateFolder(folderId: string): Promise<ApiResponse> {
try { try {
if (!folderId) { if (!folderId) {
throw new Error(`Le folderId et le nouveau titre sont requis.`); throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -473,7 +480,7 @@ class ApiService {
} }
} }
public async copyFolder(folderId: string, newTitle: string): Promise<any> { public async copyFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try { try {
if (!folderId || !newTitle) { if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`); throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -510,7 +517,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async createQuiz(title: string, content: string[], folderId: string): Promise<any> { public async createQuiz(title: string, content: string[], folderId: string): Promise<ApiResponse> {
try { try {
if (!title || !content || !folderId) { if (!title || !content || !folderId) {
@ -581,7 +588,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async deleteQuiz(quizId: string): Promise<any> { public async deleteQuiz(quizId: string): Promise<ApiResponse> {
try { try {
if (!quizId) { if (!quizId) {
@ -616,7 +623,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<any> { public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<ApiResponse> {
try { try {
if (!quizId || !newTitle || !newContent) { if (!quizId || !newTitle || !newContent) {
@ -652,7 +659,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async moveQuiz(quizId: string, newFolderId: string): Promise<any> { public async moveQuiz(quizId: string, newFolderId: string): Promise<ApiResponse> {
try { try {
if (!quizId || !newFolderId) { if (!quizId || !newFolderId) {
@ -689,7 +696,7 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async duplicateQuiz(quizId: string): Promise<any> { public async duplicateQuiz(quizId: string): Promise<ApiResponse> {
const url: string = this.constructRequestUrl(`/quiz/duplicate`); const url: string = this.constructRequestUrl(`/quiz/duplicate`);
@ -703,7 +710,7 @@ class ApiService {
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`); throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
} }
return result; return result.status === 200;
} catch (error) { } catch (error) {
console.error("Error details: ", error); console.error("Error details: ", error);
@ -723,9 +730,9 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<any> { public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<ApiResponse> {
try { try {
console.log(quizId, newTitle), folderId; console.log(quizId, newTitle, folderId);
return "Route not implemented yet!"; return "Route not implemented yet!";
} catch (error) { } catch (error) {
@ -741,7 +748,7 @@ class ApiService {
} }
} }
async ShareQuiz(quizId: string, email: string): Promise<any> { async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
try { try {
if (!quizId || !email) { if (!quizId || !email) {
throw new Error(`quizId and email are required.`); throw new Error(`quizId and email are required.`);
@ -800,7 +807,7 @@ class ApiService {
} }
} }
async receiveSharedQuiz(quizId: string, folderId: string): Promise<any> { async receiveSharedQuiz(quizId: string, folderId: string): Promise<ApiResponse> {
try { try {
if (!quizId || !folderId) { if (!quizId || !folderId) {
throw new Error(`quizId and folderId are required.`); throw new Error(`quizId and folderId are required.`);
@ -869,7 +876,8 @@ class ApiService {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const err = error as AxiosError; const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined; const data = err.response?.data as { error: string } | undefined;
return `ERROR : ${data?.error}` || 'ERROR : Erreur serveur inconnue lors de la requête.'; const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
return `ERROR : ${msg}`;
} }
return `ERROR : Une erreur inattendue s'est produite.` return `ERROR : Une erreur inattendue s'est produite.`

View file

@ -21,11 +21,20 @@ class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
connect(backendUrl: string): Socket { connect(backendUrl: string): Socket {
// console.log(backendUrl); console.log(`WebSocketService.connect('${backendUrl}')`);
this.socket = io(`${backendUrl}`, {
// // Ensure the URL uses wss: if the URL starts with https:
// const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:';
// console.log(`WebSocketService.connect: protocol=${protocol}`);
// const url = backendUrl.replace(/^http(s):/, protocol);
// console.log(`WebSocketService.connect: changed url=${url}`);
const url = backendUrl || window.location.host;
this.socket = io(url, {
transports: ['websocket'], transports: ['websocket'],
reconnectionAttempts: 1 reconnectionAttempts: 1
}); });
return this.socket; return this.socket;
} }
@ -75,14 +84,14 @@ class WebSocketService {
) { ) {
if (this.socket) { if (this.socket) {
this.socket?.emit('submit-answer', this.socket?.emit('submit-answer',
// { // {
// answer: answer, // answer: answer,
// roomName: roomName, // roomName: roomName,
// username: username, // username: username,
// idQuestion: idQuestion // idQuestion: idQuestion
// } // }
answerData answerData
); );
} }
} }
} }

View file

@ -1,4 +1,4 @@
export function escapeForGIFT(link: string): string { export function escapeForGIFT(link: string): string {
const specialChars = /[{}#~=<>\:]/g; const specialChars = /[{}#~=<>\\:]/g;
return link.replace(specialChars, (match) => `\\${match}`); return link.replace(specialChars, (match) => `\\${match}`);
} }

View file

@ -1,9 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["src/*"]
},
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ES2020", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
@ -12,7 +16,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react",
/* Linting */ /* Linting */
"strict": true, "strict": true,

View file

@ -3,22 +3,44 @@ import react from '@vitejs/plugin-react-swc';
import pluginChecker from 'vite-plugin-checker'; import pluginChecker from 'vite-plugin-checker';
import EnvironmentPlugin from 'vite-plugin-environment'; import EnvironmentPlugin from 'vite-plugin-environment';
// Filter out environment variables with invalid identifiers
const filteredEnv = Object.keys(process.env).reduce((acc, key) => {
// Only include environment variables with valid JavaScript identifiers
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
acc[key] = process.env[key];
}
return acc;
}, {});
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: "/", base: "/",
plugins: [ plugins: [
react(), react(),
pluginChecker({ typescript: true }), pluginChecker({ typescript: true }),
EnvironmentPlugin('all'), EnvironmentPlugin(filteredEnv),
], ],
resolve: {
alias: {
'src': '/src'
}
},
preview: { preview: {
port: 5173, port: 5173,
strictPort: true strictPort: true
}, },
server: { server: {
port: 5173, port: 5173,
strictPort: true, strictPort: true,
host: true, host: true,
origin: "http://0.0.0.0:5173", origin: "http://0.0.0.0:5173",
},
build: {
sourcemap: true, // Enable source maps
rollupOptions: {
output: {
sourcemapExcludeSources: true, // Exclude sources from source maps
},
},
}, },
}); });

View file

@ -1,10 +1,13 @@
version: '3'
services: services:
frontend: frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest image: fuhrmanator/evaluetonsavoir-frontend:latest
container_name: frontend container_name: frontend
environment:
# Define empty VITE_BACKEND_URL because it's production
- VITE_BACKEND_URL=
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
- VITE_BACKEND_SOCKET_URL=
ports: ports:
- "5173:5173" - "5173:5173"
restart: always restart: always
@ -49,7 +52,7 @@ services:
- mongodb_data:/data/db - mongodb_data:/data/db
restart: always restart: always
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure # Ce conteneur cherche des mises à jour à 5h du matin
watchtower: watchtower:
image: containrrr/watchtower image: containrrr/watchtower
container_name: watchtower container_name: watchtower
@ -63,6 +66,19 @@ services:
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday - WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
restart: always restart: always
watchtower-once:
image: containrrr/watchtower
container_name: watchtower-once
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --run-once
environment:
- TZ=America/Montreal
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
restart: "no"
volumes: volumes:
mongodb_data: mongodb_data:
external: false external: false

10
export-collections.bash Normal file
View file

@ -0,0 +1,10 @@
#!/bin/bash
DB_NAME="evaluetonsavoir"
OUTPUT_DIR="/data/db"
collections=$(mongosh $DB_NAME --quiet --eval "db.getCollectionNames().join(' ')")
for collection in $collections; do
mongoexport --db=$DB_NAME --collection=$collection --out=$OUTPUT_DIR/$collection.json --jsonArray
done

28
package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "EvalueTonSavoir",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dompurify": "^3.2.3"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/dompurify": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"dompurify": "^3.2.3"
}
}

View file

@ -1,4 +1,3 @@
const { create } = require('../middleware/jwtToken');
const Folders = require('../models/folders'); const Folders = require('../models/folders');
const ObjectId = require('mongodb').ObjectId; const ObjectId = require('mongodb').ObjectId;
const Quizzes = require('../models/quiz'); const Quizzes = require('../models/quiz');

View file

@ -1,3 +1,5 @@
/* eslint-disable */
// const request = require('supertest'); // const request = require('supertest');
// const app = require('../app.js'); // const app = require('../app.js');
// // const app = require('../routers/images.js'); // // const app = require('../routers/images.js');

View file

@ -2,7 +2,6 @@ const Users = require('../models/users');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const Quizzes = require('../models/quiz'); const Quizzes = require('../models/quiz');
const Folders = require('../models/folders'); const Folders = require('../models/folders');
const AppError = require('../middleware/AppError');
const { ObjectId } = require('mongodb'); const { ObjectId } = require('mongodb');
jest.mock('bcrypt'); jest.mock('bcrypt');

View file

@ -43,6 +43,7 @@ const imagesRouter = require('./routers/images.js');
// Setup environment // Setup environment
dotenv.config(); dotenv.config();
const isDev = process.env.NODE_ENV === 'development';
const errorHandler = require("./middleware/errorHandler.js"); const errorHandler = require("./middleware/errorHandler.js");
// Start app // Start app
@ -51,6 +52,7 @@ const cors = require("cors");
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const configureServer = (httpServer, isDev) => { const configureServer = (httpServer, isDev) => {
console.log(`Configuring server with isDev: ${isDev}`);
return new Server(httpServer, { return new Server(httpServer, {
path: "/socket.io", path: "/socket.io",
cors: { cors: {
@ -63,14 +65,16 @@ const configureServer = (httpServer, isDev) => {
}; };
// Start sockets (depending on the dev or prod environment) // Start sockets (depending on the dev or prod environment)
let server = http.createServer(app); const server = http.createServer(app);
let isDev = process.env.NODE_ENV === 'development';
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`); console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
const io = configureServer(server); const io = configureServer(server, isDev);
console.log(`Server configured with cors.origin: ${io.opts.cors.origin} and secure: ${io.opts.secure}`);
setupWebsocket(io); setupWebsocket(io);
console.log(`Websocket setup with on() listeners.`);
app.use(cors()); app.use(cors());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json()); app.use(bodyParser.json());

View file

@ -18,7 +18,7 @@ exports.USER_ALREADY_EXISTS = {
} }
exports.LOGIN_CREDENTIALS_ERROR = { exports.LOGIN_CREDENTIALS_ERROR = {
message: 'L\'email et le mot de passe ne correspondent pas.', message: 'L\'email et le mot de passe ne correspondent pas.',
code: 400 code: 401
} }
exports.GENERATE_PASSWORD_ERROR = { exports.GENERATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.', message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',

View file

@ -1,6 +1,6 @@
//controller //controller
const AppError = require('../middleware/AppError.js'); const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes'); const { MISSING_REQUIRED_PARAMETER, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes');
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class FoldersController { class FoldersController {

View file

@ -1,13 +1,13 @@
const emailer = require('../config/email.js'); const emailer = require('../config/email.js');
const AppError = require('../middleware/AppError.js'); const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes'); const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR } = require('../constants/errorCodes');
class QuizController { class QuizController {
constructor(quizModel, foldersModel) { constructor(quizModel, foldersModel) {
this.folders = foldersModel;
this.quizzes = quizModel; this.quizzes = quizModel;
this.folders = foldersModel;
} }
create = async (req, res, next) => { create = async (req, res, next) => {
@ -165,7 +165,7 @@ class QuizController {
} }
}; };
copy = async (req, res, next) => { copy = async (req, _res, _next) => {
const { quizId, newTitle, folderId } = req.body; const { quizId, newTitle, folderId } = req.body;
if (!quizId || !newTitle || !folderId) { if (!quizId || !newTitle || !folderId) {
@ -207,7 +207,7 @@ class QuizController {
} }
// Call the method from the Quiz model to delete quizzes by folder ID // Call the method from the Quiz model to delete quizzes by folder ID
await Quiz.deleteQuizzesByFolderId(folderId); await this.quizzes.deleteQuizzesByFolderId(folderId);
return res.status(200).json({ return res.status(200).json({
message: 'Quizzes deleted successfully.' message: 'Quizzes deleted successfully.'
@ -232,7 +232,7 @@ class QuizController {
try { try {
const existingFile = await this.quizzes.quizExists(title, userId); const existingFile = await this.quizzes.quizExists(title, userId);
return existingFile !== null; return existingFile !== null;
} catch (error) { } catch (_error) {
throw new AppError(GETTING_QUIZ_ERROR); throw new AppError(GETTING_QUIZ_ERROR);
} }
}; };

32
server/eslint.config.mjs Normal file
View file

@ -0,0 +1,32 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
...globals.jest, // Add Jest globals
},
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrors": "all", // Ignore all catch clause parameters
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
},
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
pluginJs.configs.recommended,
];

View file

@ -1,7 +1,7 @@
const AppError = require("./AppError"); const AppError = require("./AppError");
const fs = require('fs'); const fs = require('fs');
const errorHandler = (error, req, res, next) => { const errorHandler = (error, req, res) => {
console.log("ERROR", error); console.log("ERROR", error);
if (error instanceof AppError) { if (error instanceof AppError) {

View file

@ -1,4 +1,3 @@
//const db = require('../config/db.js')
const { ObjectId } = require('mongodb'); const { ObjectId } = require('mongodb');
class Images { class Images {
@ -8,8 +7,8 @@ class Images {
} }
async upload(file, userId) { async upload(file, userId) {
await db.connect() await this.db.connect()
const conn = db.getConnection(); const conn = this.db.getConnection();
const imagesCollection = conn.collection('images'); const imagesCollection = conn.collection('images');
@ -27,8 +26,8 @@ class Images {
} }
async get(id) { async get(id) {
await db.connect() await this.db.connect()
const conn = db.getConnection(); const conn = this.db.getConnection();
const imagesCollection = conn.collection('images'); const imagesCollection = conn.collection('images');

871
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,10 @@
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9.18.0",
"globals": "^15.14.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-mock": "^29.7.0", "jest-mock": "^29.7.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",

View file

@ -16,12 +16,12 @@ const setupWebsocket = (io) => {
} }
totalConnections++; totalConnections++;
// console.log( console.log(
// "A user connected:", "A user connected:",
// socket.id, socket.id,
// "| Total connections:", "| Total connections:",
// totalConnections totalConnections
// ); );
socket.on("create-room", (sentRoomName) => { socket.on("create-room", (sentRoomName) => {
if (sentRoomName) { if (sentRoomName) {